Write k6 tests in golang
The xk6-g0 extension allows writing k6 tests in the go language.
script.go
package main
import "net/http"
func Default() {
http.Get("https://test.k6.io")
}
run
./k6 run script.go
Although k6's officially supported scripting language is JavaScript, support for other languages appears from time to time. In this blog post, you can read an understandable and clear explanation of why k6 officially supports only JavaScript language: Why k6 does not support multiple scripting languages?
xk6-g0 is an experiment to use the go programming language as a full-fledged script language in k6 tests with the support of the community. Since k6 extensions (including xk6-g0) are made in the go language, every xk6-g0 user is also a potential contributor to xk6-g0 development. If the community really wants to use the go programming language to write k6 tests, hopefully they will be committed enough to contribute to xk6-g0 development. Otherwise, xk6-g0 remains an interesting experiment.
When using xk6-g0, the tests are executed by a built-in go interpreter (yaegi), so there is no need for a compilation or build phase. It is true that the speed of interpreted execution does not reach the speed of compiled code, but it has many advantages. On the other hand, even with JavaScript support, the interpreter performs the tests.
Accepting the benchmark measurement made with tengo's developer, the JavaScript (goja) interpreter calculates Fibonacci numbers twice as fast as the go interpreter (yaegi). Well, it is relatively rare to count fibonacci numbers in tests, so we are not far off with the approximation that there is no double multiplier in execution speed. (someday a more accurate measurement would be useful)
The go script should be put in the main
package. The following lifecycle callback functions and configuration object can be exported by the script:
- Setup: corresponds to the setup function of the JavaScript API
- TearDown: corresponds to the teardown function of the JavaScript API
- Default: corresponds to the default export function of the JavaScript API
- HandleSummary: corresponds to the handleSummary function of the JavaScript
- Options: corresponds to the JavaScript API options object
The return values of the lifecycle callback functions are optional, they can also be defined without a return value. The function parameters are also optional, but their order is fixed.
The script is executed similarly to the JavaScript language:
./k6 run scripts/simple/script.go
Setup
function corresponds to the setup function of the JavaScript API.
func Setup() (interface{}, error)
The return values are optional, i.e. in addition to the above form, the following can also be used:
func Setup() interface{}
func Setup() error
func Setup()
Optionally, the following parameters can also be used:
- context.Context execution context
- assert.Assertions provides assertion methods
- require.Assertions provides assertion methods
func Setup(context.Context, assert.Assertions) (interface{}, error)
TearDown
function corresponds to the teardown function of the JavaScript API.
func Teardown(data interface{}) error
The parameter and the return value are optional, i.e. in addition to the above form, the following can also be used:
func Teardown(data interface{})
func Teardown() error
func Teardown()
Optionally, the following parameters can also be used:
- context.Context execution context
- assert.Assertions provides assertion methods
- require.Assertions provides assertion methods
func Teardown(ctx context.Context, assert assert.Assertions, data interface{}) error
Default
function corresponds to the default export function of the JavaScript API.
func Default(data interface{}) error
The parameter and the return value are optional, i.e. in addition to the above form, the following can also be used:
func Default(data interface{})
func Default() error
func Default()
Optionally, the following parameters can also be used:
- context.Context execution context
- assert.Assertions provides assertion methods
- require.Assertions provides assertion methods
func Default(ctx context.Context, assert assert.Assertions, data interface{}) error
HandleSummary
function corresponds to the handleSummary function of the JavaScript.
func HandleSummary(data map[string]interface{}) (map[string]interface{}, error)
The error return value is optional, i.e. in addition to the above form, the following can also be used:
func HandleSummary(data map[string]interface{}) map[string]interface{}
Options
variable corresponds to the JavaScript API options object.
var Options map[string]interface{}
The xk6-g0 is currently in Proof of Concept status. The further fate of the development depends on the community's feedback on the usefulness of the concept.
Is it useful to support the go language (yaegi interpreter) in k6 tests? You can vote here: #1
The primary API design consideration: don't have an API at all.
There are many popular packages for the go programming language, xk6-g0 tries to implement the necessary functionality by integrating and supporting these packages without its own API. This approach has many advantages, such as:
- the test writer does not need to learn a new API
- test scripts can be tested using standard go testing
In addition to the go standard library, the following third-party packages can be used:
- https://github.com/go-resty/resty
- https://github.com/sirupsen/logrus
- https://github.com/stretchr/testify
- https://github.com/PuerkitoBio/goquery
- https://github.com/tidwall/gjson
- https://github.com/PaesslerAG/jsonpath
- https://github.com/santhosh-tekuri/jsonschema/v5
- https://github.com/brianvoe/gofakeit/v6
The Default
function's optional assert.Assertions or require.Assertions parameters can be used to define the k6 checks. The name of the check will be the message parameter of the corresponding assertion function.
Of course, metrics are also created from the checks defined in this way.
package main
import (
"net/http"
"github.com/stretchr/testify/assert"
)
func Default(assert *assert.Assertions) {
res, err := http.Get("https://httpbin.test.k6.io/get")
assert.NoError(err, "got response without error")
assert.Equal(http.StatusOK, res.StatusCode, "status code was 200")
assert.Equal("application/json", res.Header.Get("Content-Type"), "content type was application/json")
}
output
/\ |‾‾| /‾‾/ /‾‾/
/\ / \ | |/ / / /
/ \/ \ | ( / ‾‾\
/ \ | |\ \ | (‾) |
/ __________ \ |__| \__\ \_____/ .io
execution: local
script: -
output: -
scenarios: (100.00%) 1 scenario, 1 max VUs, 10m30s max duration (incl. graceful stop):
* default: 1 iterations for each of 1 VUs (maxDuration: 10m0s, gracefulStop: 30s)
✓ got response without error
✓ status code was 200
✓ content type was application/json
checks.....................: 100.00% ✓ 3 ✗ 0
data_received..............: 6.0 kB 14 kB/s
data_sent..................: 457 B 1.1 kB/s
http_req_blocked...........: avg=302.7ms min=302.7ms med=302.7ms max=302.7ms p(90)=302.7ms p(95)=302.7ms
http_req_connecting........: avg=124.32ms min=124.32ms med=124.32ms max=124.32ms p(90)=124.32ms p(95)=124.32ms
http_req_duration..........: avg=126.6ms min=126.6ms med=126.6ms max=126.6ms p(90)=126.6ms p(95)=126.6ms
http_req_receiving.........: avg=355.31µs min=355.31µs med=355.31µs max=355.31µs p(90)=355.31µs p(95)=355.31µs
http_req_sending...........: avg=54.2µs min=54.2µs med=54.2µs max=54.2µs p(90)=54.2µs p(95)=54.2µs
http_req_tls_handshaking...: avg=151.39ms min=151.39ms med=151.39ms max=151.39ms p(90)=151.39ms p(95)=151.39ms
http_req_waiting...........: avg=126.19ms min=126.19ms med=126.19ms max=126.19ms p(90)=126.19ms p(95)=126.19ms
http_reqs..................: 1 2.326236/s
iteration_duration.........: avg=429.68ms min=429.68ms med=429.68ms max=429.68ms p(90)=429.68ms p(95)=429.68ms
iterations.................: 1 2.326236/s
From the http package of the standard library, metrics are created on the use of the following:
- http.DefaultClient
- http.Get, http.Head, http.Post, http.PostForm
In addition, the https://github.com/go-resty/resty HTTP client can also be used, metrics are generated from its use.
package main
import "github.com/go-resty/resty/v2"
func Default() error {
_, err := client.R().Get("https://httpbin.test.k6.io/get")
return err
}
var client *resty.Client
func init() {
client = resty.New()
}
HTML documents can be parsed and manipulated using the popular github.com/PuerkitoBio/goquery package, which brings a syntax and a set of features similar to jQuery to the Go language.
package main
import (
"github.com/PuerkitoBio/goquery"
"github.com/sirupsen/logrus"
)
func Default() error {
doc, err := goquery.NewDocument("https://test.k6.io")
if err != nil {
return err
}
logrus.Info(doc.Find("h1.title span.text-blue").Text())
return nil
}
The gjson and jsonpath packages can be used to query JSON documents.
gjson
package main
import (
"net/http"
"github.com/go-resty/resty/v2"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
)
func Default(require *require.Assertions) {
res, err := resty.New().R().Get("https://httpbin.test.k6.io/get")
require.NoError(err, "request success")
require.Equal(http.StatusOK, res.StatusCode(), "status code 200")
body := res.Body()
val := gjson.GetBytes(body, "headers.Host").Str
require.Equal("httpbin.test.k6.io", val, "headers.Host value OK")
}
jsonpath
package main
import (
"net/http"
"github.com/PaesslerAG/jsonpath"
"github.com/go-resty/resty/v2"
"github.com/stretchr/testify/require"
)
func Default(require *require.Assertions) {
body := make(map[string]interface{})
res, err := resty.New().R().SetResult(&body).Get("https://httpbin.test.k6.io/get")
require.NoError(err, "request success")
require.Equal(http.StatusOK, res.StatusCode(), "status code 200")
val, err := jsonpath.Get("$.headers.Host", body)
require.NoError(err, "$.headers.Host no error")
require.Equal("httpbin.test.k6.io", val, "$.headers.Host value OK")
}
The https://github.com/sirupsen/logrus package can be used for logging in the test script.
package main
import "github.com/sirupsen/logrus"
func Setup() interface{} {
logrus.Info("Setup")
return map[string]interface{}{
"foo": "bar",
}
}
func Default(data interface{}) {
logrus.Info("Default", data)
}
func Teardown(data interface{}) {
logrus.Info("Teardown", data)
}
func init() {
logrus.Info("init")
}
The first parameter of the Default
function is optionally a context.Context. This can be used to perform context aware operations and to access various context variables.
The usual k6 variables (eg __VU
, __ENV
, __ITER
) and the variables of the k6/execution
module can be accessed using the Value
function of the context parameter.
package main
import (
"context"
"github.com/sirupsen/logrus"
)
func Default(ctx context.Context) {
vu := ctx.Value("__VU").(int64)
env := ctx.Value("__ENV").(map[string]string)
iter := ctx.Value("__ITER").(int64)
logrus.Info(vu)
logrus.Info(iter)
logrus.Info(env["PATH"])
logrus.Info(ctx.Value("execution.scenario.name"))
}
You can download pre-built k6 binaries from Releases page. Check Packages page for pre-built k6 Docker images.
You can build the k6 binary on various platforms, each with its requirements. The following shows how to build k6 binary with this extension on GNU/Linux distributions.
You must have the latest Go version installed to build the k6 binary. The latest version should match k6 and xk6.
-
Install
xk6
:go install go.k6.io/xk6/cmd/xk6@latest
-
Build the binary:
xk6 build --with github.com/szkiba/xk6-g0@latest
Note You can always use the latest version of k6 to build the extension, but the earliest version of k6 that supports extensions via xk6 is v0.43.1. The xk6 is constantly evolving, so some APIs may not be backward compatible.
If you want to add a feature or make a fix, clone the project and build it using the following commands. The xk6 will force the build to use the local clone instead of fetching the latest version from the repository. This process enables you to update the code and test it locally.
git clone [email protected]:szkiba/xk6-g0.git && cd xk6-g0
xk6 build --with github.com/szkiba/xk6-g0@latest=.
You can also use pre-built k6 image within a Docker container. In order to do that, you will need to execute something like the following:
Linux
docker run -v $(pwd):/work -it --rm ghcr.io/szkiba/xk6-g0:latest run /work/scripts/simple/script.go
Windows
docker run -v %cd%:/work -it --rm ghcr.io/szkiba/xk6-g0:latest run /work/scripts/simple/script.go
There are many examples in the scripts directory that show how to use various features of the extension.
xk6-g0 allows you to install additional packages in addition to the built-in go packages without changing the xk6-g0 source code. For this, for example, a function must be registered from the init() function of a custom k6 extension, which can be used to make additional packages available.
Check xk6-g0-figure as an example addon.