No results for

Powered byAlgolia

JavaScript Extensions

Take advantage of Go's speed, power, and efficiency while providing the flexibility of using JavaScript APIs within your test scripts.

By implementing k6 interfaces, you can close various gaps in your testing setup:

  • New network protocols
  • Improved performance
  • Features not supported by k6 core

Before you start

To run this tutorial, you'll need the following applications installed:

  • Go
  • Git

You also need to install xk6:

$ go install go.k6.io/xk6/cmd/xk6@latest

Write a simple extension

  1. First, set up a directory to work in:

    $ mkdir xk6-compare; cd xk6-compare; go mod init xk6-compare
  2. In the directory, make a Go file for your JavaScript extension.

    A simple JavaScript extension requires a struct that exposes methods called by the test script.

    package compare
    import "fmt"
    // Compare is the type for our custom API.
    type Compare struct{
    ComparisonResult string // textual description of the most recent comparison
    }
    // IsGreater returns true if a is greater than b, or false otherwise, setting textual result message.
    func (c *Compare) IsGreater(a, b int) bool {
    if a > b {
    c.ComparisonResult = fmt.Sprintf("%d is greater than %d", a, b)
    return true
    } else {
    c.ComparisonResult = fmt.Sprintf("%d is NOT greater than %d", a, b)
    return false
    }
    }
  3. Register the module to use these from k6 test scripts.

    note

    k6 extensions must have the k6/x/ prefix, and the short name must be unique among all extensions built in the same k6 binary.

    import "go.k6.io/k6/js/modules"
    // init is called by the Go runtime at application startup.
    func init() {
    modules.Register("k6/x/compare", new(Compare))
    }
  4. Save the file as something like compare.go. The final code looks like this:

    compare.go
    1package compare
    2
    3import (
    4 "fmt"
    5 "go.k6.io/k6/js/modules"
    6)
    7
    8// init is called by the Go runtime at application startup.
    9func init() {
    10 modules.Register("k6/x/compare", new(Compare))
    11}
    12
    13// Compare is the type for our custom API.
    14type Compare struct{
    15 ComparisonResult string // textual description of the most recent comparison
    16}
    17
    18// IsGreater returns true if a is greater than b, or false otherwise, setting textual result message.
    19func (c *Compare) IsGreater(a, b int) bool {
    20 if a > b {
    21 c.ComparisonResult = fmt.Sprintf("%d is greater than %d", a, b)
    22 return true
    23 } else {
    24 c.ComparisonResult = fmt.Sprintf("%d is NOT greater than %d", a, b)
    25 return false
    26 }
    27}

Compile your extended k6

To build a k6 binary with this extension, run this command:

$ xk6 build --with xk6-compare=.
note

When building from source code, xk6-compare is the Go module name passed to go mod init. Usually, this would be a URL similar to github.com/grafana/xk6-compare.

Use your extension

Now, use the extension in a test script!

  1. Make a file with a name like test.js then add this code:

    test.js
    1import compare from 'k6/x/compare';
    2
    3export default function () {
    4 console.log(`${compare.isGreater(2, 1)}, ${compare.comparison_result}`);
    5 console.log(`${compare.isGreater(1, 3)}, ${compare.comparison_result}`);
    6}
  2. Run the test with ./k6 run test.js.

    It should output the following:

    INFO[0000] true, 2 is greater than 1 source=console
    INFO[0000] false, 1 is NOT greater than 3 source=console

Use the advanced module API

Suppose your extension needs access to internal k6 objects to, for example, inspect the state of the test during execution. We will need to make slightly more complicated changes to the above example.

Our main Compare struct should implement the modules.Instance interface to access the modules.VU to inspect internal k6 objects such as:

Additionally, there should be a root module implementation of the modules.Module interface to serve as a factory of Compare instances for each VU.

The compare factory can have memory implications

The significance depends on the size of your module.

Here's what that would look like:

compare.go
1package compare
2
3import (
4 "fmt"
5 "go.k6.io/k6/js/modules"
6)
7
8// init is called by the Go runtime at application startup.
9func init() {
10 modules.Register("k6/x/compare", New())
11}
12
13type (
14 // RootModule is the global module instance that will create module
15 // instances for each VU.
16 RootModule struct{}
17
18 // ModuleInstance represents an instance of the JS module.
19 ModuleInstance struct {
20 // vu provides methods for accessing internal k6 objects for a VU
21 vu modules.VU
22 // comparator is the exported type
23 comparator *Compare
24 }
25)
26
27// Ensure the interfaces are implemented correctly.
28var (
29 _ modules.Instance = &ModuleInstance{}
30 _ modules.Module = &RootModule{}
31)
32
33// New returns a pointer to a new RootModule instance.
34func New() *RootModule {
35 return &RootModule{}
36}
37
38// NewModuleInstance implements the modules.Module interface returning a new instance for each VU.
39func (*RootModule) NewModuleInstance(vu modules.VU) modules.Instance {
40 return &ModuleInstance{
41 vu: vu,
42 comparator: &Compare{vu: vu}
43 }
44}
45
46// Compare is the type for our custom API.
47type Compare struct{
48 vu modules.VU // provides methods for accessing internal k6 objects
49 ComparisonResult string // textual description of the most recent comparison
50}
51
52// IsGreater returns true if a is greater than b, or false otherwise, setting textual result message.
53func (c *Compare) IsGreater(a, b int) bool {
54 if a > b {
55 c.ComparisonResult = fmt.Sprintf("%d is greater than %d", a, b)
56 return true
57 } else {
58 c.ComparisonResult = fmt.Sprintf("%d is NOT greater than %d", a, b)
59 return false
60 }
61}
62
63// Exports implements the modules.Instance interface and returns the exported types for the JS module.
64func (mi *ModuleInstance) Exports() modules.Exports {
65 return modules.Exports{
66 Default: c.comparator
67 }
68}
note

Notice that we implemented the Module API and now modules.Register the root module rather than our Compare object!

Accessing runtime state

At this time, we've provided access to the modules.VU from the Compare type; however, we aren't taking advantage of the methods provided. Here is a contrived example of how we can utilize the runtime state:

// InternalState holds basic metadata from the runtime state.
type InternalState struct {
ActiveVUs int64 `js:"activeVUs"`
Iteration int64
VUID uint64 `js:"vuID"`
VUIDFromRuntime goja.Value `js:"vuIDFromRuntime"`
}
// GetInternalState interrogates the current virtual user for state information.
func (c *Compare) GetInternalState() *InternalState {
state := c.vu.State()
ctx := c.vu.Context()
es := lib.GetExecutionState(ctx)
rt := c.vu.Runtime()
return &InternalState{
VUID: state.VUID,
VUIDFromRuntime: rt.Get("__VU"),
Iteration: state.Iteration,
ActiveVUs: es.GetCurrentlyActiveVUsCount(),
}
}

Create a test script to utilize the new getInternalState() function as in the following:

test-state.js
1import compare from 'k6/x/compare';
2
3export default function () {
4 const state = compare.getInternalState();
5 console.log(
6 `Active VUs: ${state.activeVUs}, Iteration: ${state.iteration}, VU ID: ${state.vuID}, VU ID from runtime: ${state.vuIDFromRuntime}`
7 );
8}

Executing the script as ./k6 run test-state.js --vus 2 --iterations 5 will produce output similar to the following:

INFO[0000] Active VUs: 2, Iteration: 0, VU ID: 2, VU ID from runtime: 2 source=console
INFO[0000] Active VUs: 2, Iteration: 0, VU ID: 1, VU ID from runtime: 1 source=console
INFO[0000] Active VUs: 2, Iteration: 1, VU ID: 2, VU ID from runtime: 2 source=console
INFO[0000] Active VUs: 2, Iteration: 1, VU ID: 1, VU ID from runtime: 1 source=console
INFO[0000] Active VUs: 2, Iteration: 2, VU ID: 2, VU ID from runtime: 2 source=console

For a more extensive usage example of this API, look at the k6/execution module.

Things to keep in mind

  • The code in the default function (or another function specified by exec) will be executed many times during a test run and possibly in parallel by thousands of VUs. Any operation of your extension should therefore be performant and thread-safe.
  • Any heavy initialization should be done in the init context, if possible, and not as part of the default function execution.
  • Use the registry's NewMetric method to create custom metrics; to emit them, use metrics.PushIfNotDone().

Questions? Feel free to join the discussion on extensions in the k6 Community Forum.

Next, create an Output extension to publish test metrics to a destination not already supported by k6.