You can import and reuse JavaScript code across your k6 load test scripts, treating them like modules.

Let’s say you have a common set of utility functions, like generating unique user IDs or formatting timestamps, that you want to use in multiple load test scenarios. Instead of copying and pasting that code into every script, you can define it in a separate JavaScript file and then import it into your main k6 test scripts.

Here’s a simple example.

1. Create a Utility Module (utils.js)

// utils.js
export function generateUserId(prefix) {
  return `${prefix}-${Math.random().toString(36).substr(2, 9)}`;
}

export function formatTimestamp(date) {
  return date.toISOString();
}

This utils.js file defines two functions, generateUserId and formatTimestamp, and exports them using the export keyword. This makes them available for other JavaScript modules to import.

2. Create a Main k6 Test Script (test.js)

// test.js
import { generateUserId, formatTimestamp } from './utils.js';

export const options = {
  vus: 10,
  duration: '30s',
};

export default function () {
  const userId = generateUserId('testuser');
  const now = new Date();

  console.log(`User ID: ${userId}`);
  console.log(`Timestamp: ${formatTimestamp(now)}`);

  // Simulate some HTTP request
  // http.get('https://your.api.com/data');

  // sleep(1); // Optional: pause between iterations
}

In test.js, we use the import statement to bring in the functions from utils.js. The path ./utils.js is relative to the location of test.js. You can then use generateUserId and formatTimestamp directly in your test.

3. Run the Test

You can run this test from your terminal:

k6 run test.js

You’ll see output like this for each iteration:

INFO[0000] User ID: testuser-a1b2c3d4e
INFO[0000] Timestamp: 2023-10-27T10:30:00.123Z

The Problem This Solves

The primary problem this addresses is code duplication and lack of maintainability. Without modules, common logic would be scattered across many test files. If you needed to fix a bug or update a utility function, you’d have to find and modify it in every single script, a tedious and error-prone process. Modules allow you to centralize shared logic, making your test suites cleaner, more organized, and much easier to manage.

Internal Mechanics: ES Modules in k6

k6 leverages the ES Modules (ECMAScript Modules) standard for JavaScript. When k6 encounters an import statement, it performs the following:

  1. Resolution: It resolves the imported module’s path. This can be a relative path (like ./utils.js), an absolute path, or even a URL.
  2. Loading: It loads the content of the module file.
  3. Execution: It executes the module’s code. Any export declarations make functions, variables, or classes available.
  4. Linking: It links the imported names from the module into the importing script’s scope.

This process happens before your main default function or any other exported test functions begin executing. k6’s JavaScript runtime (V8) handles this modularity efficiently.

Configuration and Import Paths

You can use relative paths (./, ../) or absolute paths. For more complex projects or when dealing with dependencies installed via npm (which k6 also supports through its --env flag and k6-bundle tool), you might import from node_modules.

For example, if you had a shared library in a lib directory at the same level as your test scripts:

project/
├── tests/
│   ├── api_test.js
│   └── ui_test.js
└── lib/
    └── common_assertions.js

In tests/api_test.js, you could import from lib:

// tests/api_test.js
import { check } from '../lib/common_assertions.js'; // Relative path

export default function () {
  // ... your test logic ...
  // check(response, { 'status is 200': (r) => r.status === 200 });
}

Organizing Large Test Suites

As your test suites grow, you’ll likely want a more structured approach. A common pattern is to have a dedicated directory for shared modules:

load-tests/
├── tests/
│   ├── feature_a/
│   │   └── login_test.js
│   ├── feature_b/
│   │   └── product_page_test.js
│   └── common/
│       ├── api_client.js
│       ├── data_generators.js
│       └── assertions.js
└── k6.js  // Main entry point if needed, or just run individual tests

Then, within tests/feature_a/login_test.js:

// tests/feature_a/login_test.js
import { apiClient } from '../common/api_client.js';
import { generateUserData } from '../common/data_generators.js';

export default function () {
  const user = generateUserData();
  const response = apiClient.post('/login', user);
  // ... assertions on response ...
}

This hierarchical structure makes it clear where shared code resides and how it’s being used.

The One Thing Most People Don’t Know

You can import modules directly from URLs. This is incredibly useful for sharing common setup code or for using libraries hosted externally without needing to manage local copies or complex build processes. For instance, you could import a shared k6 extension or a common utility library from a CDN or a private Git repository that serves raw file content. This allows for dynamic updating of shared logic without redeploying your test scripts themselves, provided the URL remains stable.

The next step in making your load tests more robust is exploring how to manage and parameterize data, such as user credentials or test data sets, across different test runs and virtual users.

Want structured learning?

Take the full K6 course →