You can build custom extensions for k6, called xk6 modules, using Go.
Let’s see it in action. Imagine you have a service that uses a custom authentication scheme based on time-signed tokens, and you want to simulate this in your k6 tests. You’d write a Go module that generates these tokens and then expose it to your JavaScript tests.
Here’s a simplified main.go for an xk6 module that generates a time-signed token:
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"strings"
"time"
"go.k6.io/k6/js/modules"
)
func init() {
modules.Register("k6/x/auth", new(Auth))
}
type Auth struct{}
func (a *Auth) GenerateToken(secret string, expiryMinutes int) (string, error) {
if secret == "" {
return "", fmt.Errorf("secret cannot be empty")
}
if expiryMinutes <= 0 {
return "", fmt.Errorf("expiryMinutes must be positive")
}
timestamp := time.Now().Unix()
expiry := timestamp + int64(expiryMinutes*60)
payload := fmt.Sprintf("%d:%d", timestamp, expiry)
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(payload))
signature := hex.EncodeToString(mac.Sum(nil))
return fmt.Sprintf("%s.%s", payload, signature), nil
}
func main() {
// This is required for the extension to be built as a Go program
// but it's not actually executed during k6 runtime.
}
To build this, you’d typically use the k6 CLI:
go install go.k6.io/k6/cmd/k6@latest
go install github.com/grafana/xk6/cmd/xk6@latest
xk6 build --output k6-custom --extensions github.com/your-repo/your-auth-module
(Note: Replace github.com/your-repo/your-auth-module with the actual path to your Go module if it’s in a repository. If it’s a local module, you’d build it differently, often by having the main.go in your project and running xk6 build from that directory.)
Once built, you can use your custom module in a k6 JavaScript test script:
import http from 'k6/http';
import { check } from 'k6';
import auth from 'k6/x/auth'; // Import your custom module
const SECRET_KEY = 'my-super-secret-key'; // In a real scenario, load this securely
export const options = {
vus: 10,
duration: '30s',
};
export default function () {
// Generate a token that expires in 5 minutes
const token = auth.GenerateToken(SECRET_KEY, 5);
const params = {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
};
const res = http.get('https://your.api.com/resource', params);
check(res, {
'status is 200': (r) => r.status === 200,
'token generated and used': (r) => r.request.headers.Authorization !== undefined,
});
}
This xk6 build command compiles your Go code into a standalone k6 binary. When k6 runs this binary, it embeds your Go module, making it available for import in your JavaScript test scripts just like any built-in module. The modules.Register("k6/x/auth", new(Auth)) line in your Go code is crucial; it tells k6 how to find and instantiate your custom module, mapping the Go struct Auth to the JavaScript import path k6/x/auth.
The core problem xk6 extensions solve is bridging the gap between k6’s JavaScript environment and the power of Go. While k6’s JavaScript API is rich, there are operations that are significantly more efficient or simply impossible to implement in JavaScript alone. This includes low-level network manipulation, complex cryptographic operations, direct hardware access, or integrating with existing Go libraries. xk6 allows you to write these performance-critical or specialized parts in Go and expose them as clean JavaScript APIs, effectively extending k6’s capabilities without sacrificing the ease of use of JavaScript for test scripting.
When you call auth.GenerateToken in JavaScript, k6’s runtime finds the registered Go module, calls the GenerateToken method on the Go Auth struct, passes the arguments (a string and an integer), and returns the result (a string and potentially an error) back to the JavaScript environment. This seamless interop is what makes xk6 so powerful.
The most surprising thing about xk6 modules is that you’re not just adding new functions; you’re essentially creating new native JavaScript objects and methods that k6’s runtime can understand and execute. This means you can define complex structures, asynchronous operations (though often simpler to expose sync functions and handle async in JS), and even custom error types that behave just like built-in k6 errors. This deep integration allows for highly specialized tooling that feels like a natural extension of k6 itself, rather than an external library bolted on.
The next concept you’ll likely encounter is managing state and configuration across multiple instances of your custom module or within a single test execution.