Skip to main content

Test Structure

Every k6 test script follows a consistent structure with specific lifecycle functions. Understanding this structure is essential for writing effective load tests.

Basic Test Anatomy

A k6 test consists of four main parts:
import http from 'k6/http';
import { check, sleep } from 'k6';

// 1. Init code - runs once per VU
export let options = {
  vus: 10,
  duration: '30s',
};

// 2. Setup code - runs once at test start
export function setup() {
  // Prepare test data
  let res = http.get('https://quickpizza.grafana.com/api/auth');
  return { token: res.json('token') };
}

// 3. VU code - runs repeatedly for each VU
export default function (data) {
  let res = http.get('https://quickpizza.grafana.com', {
    headers: { 'Authorization': `Bearer ${data.token}` }
  });
  check(res, { 'status is 200': (r) => r.status === 200 });
  sleep(1);
}

// 4. Teardown code - runs once at test end
export function teardown(data) {
  // Clean up test data
  console.log('Test completed with token:', data.token);
}

Lifecycle Phases

Init Context

The init context runs once per VU at the beginning of the test. Use it for:
  • Importing modules
  • Loading local files
  • Defining test options
  • Initializing custom metrics
Code in the init context cannot make HTTP requests. HTTP calls are only allowed in setup, default, and teardown functions.
import { Counter } from 'k6/metrics';
import { SharedArray } from 'k6/data';

// Init context - runs once per VU
const users = new SharedArray('users', function() {
  return JSON.parse(open('./users.json'));
});

const myCounter = new Counter('my_counter');

export default function() {
  // VU context - can access init variables
  myCounter.add(1);
}

Setup Function

The setup function runs once before the test execution begins. It’s ideal for:
  • Authenticating and retrieving tokens
  • Creating test data
  • Preparing the test environment
export function setup() {
  // Runs once before the test
  let loginRes = http.post('https://quickpizza.grafana.com/api/login', {
    username: '[email protected]',
    password: 'password123'
  });
  
  return {
    authToken: loginRes.json('token'),
    startTime: new Date().toISOString()
  };
}
Data returned from setup() is passed to the default function and teardown() function as the first parameter.

Default Function (VU Code)

The default function is the main test logic that each VU executes repeatedly. This is where you:
  • Make HTTP requests
  • Validate responses with checks
  • Add think time with sleep
  • Record custom metrics
export default function () {
  http.get('https://quickpizza.grafana.com');
}

Teardown Function

The teardown function runs once at the end of the test. Use it for:
  • Cleaning up test data
  • Logging final results
  • Deleting resources
export function teardown(data) {
  // Clean up created resources
  http.del(`https://quickpizza.grafana.com/api/sessions/${data.sessionId}`, {
    headers: { 'Authorization': `Bearer ${data.authToken}` }
  });
}

Execution Order

Here’s how k6 executes your test:
1. Init code (once per VU)
2. Setup function (once)
3. Default function (repeatedly by each VU)
4. Teardown function (once)
If you have 10 VUs, the init code runs 10 times (once per VU), but setup and teardown run only once for the entire test.

Multiple Exported Functions

You can export multiple test functions and use them with scenarios:
export function listOrders() {
  http.get('https://quickpizza.grafana.com/api/orders');
}

export function createOrder() {
  http.post('https://quickpizza.grafana.com/api/orders', {
    items: ['pizza', 'soda']
  });
}

export let options = {
  scenarios: {
    list_scenario: {
      executor: 'constant-vus',
      exec: 'listOrders',
      vus: 10,
      duration: '30s',
    },
    create_scenario: {
      executor: 'constant-vus',
      exec: 'createOrder',
      vus: 5,
      duration: '30s',
    },
  },
};

Best Practices

  1. Keep init code lightweight - It runs for every VU, so avoid heavy computations
  2. Use setup for authentication - Get tokens once, not in every iteration
  3. Add sleep in default function - Simulate realistic user behavior
  4. Return data from setup - Share authentication tokens and test data
  5. Clean up in teardown - Remove test data to avoid pollution

Controlling Setup/Teardown

You can skip setup or teardown using options:
export let options = {
  noSetup: true,      // Skip setup function
  noTeardown: true,   // Skip teardown function
  setupTimeout: '60s',    // Timeout for setup (default: 10s)
  teardownTimeout: '60s', // Timeout for teardown (default: 10s)
};

Next Steps

Build docs developers (and LLMs) love