Skip to content

szkiba/xk6-g0

Repository files navigation

xk6-g0

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)

Usage

The go script should be put in the main package. The following lifecycle callback functions and configuration object can be exported by the script:

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

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:

func Setup(context.Context, assert.Assertions) (interface{}, error)

Teardown

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:

func Teardown(ctx context.Context, assert assert.Assertions, data interface{}) error

Default

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:

func Default(ctx context.Context, assert assert.Assertions, data interface{}) error

HandleSummary

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

Options variable corresponds to the JavaScript API options object.

var Options map[string]interface{}

Roadmap

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

API

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:

Checks

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

HTTP client

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

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
}

JSON

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")
}

Logging

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")
}

Context

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"))
}

Download

You can download pre-built k6 binaries from Releases page. Check Packages page for pre-built k6 Docker images.

Build

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.

Prerequisites

You must have the latest Go version installed to build the k6 binary. The latest version should match k6 and xk6.

  • Git for cloning the project
  • xk6 for building k6 binary with extensions

Install and build the latest tagged version

  1. Install xk6:

    go install go.k6.io/xk6/cmd/xk6@latest
  2. 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.

Build for development

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=.

Docker

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

Example scripts

There are many examples in the scripts directory that show how to use various features of the extension.

Extending xk6-g0

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.