Setup and Teardown running by Virtual User?

In a lot of cases, you can easily log in and log out (or create and destroy) with multiple user accounts in setup() and teardown(). This can be done by just having a higher setupTimeout / teardownTimeout values and/or making the HTTP requests with http.batch() and returning the resulting credentials in an array. Then each VUs can just pick its own credentials from that array by using the __VU execution context variable and some modulo arithmetic, somewhat like this:

export default function(setupData) {
  let myCredentials = setupData[__VU % options.vus];
  // ...
}

That said, the recently released k6 v0.27.0 also adds another option - the per-vu-iterations executor. It, and fact that you can have multiple sequential scenarios, combined with the property that k6 will reuse VUs between non-overlapping scenarios, means that you can make something like a per-VU initialization function! :tada: This can be done by just having a scenario that does 1 iteration per VU and persisting any data or credentials in the global scope. Here’s a very complicated example that demonstrates this workaround, and it even includes thresholds to make sure that the per-VU setup and teardown methods are executed correctly:

import http from 'k6/http';
import { sleep, check } from 'k6';
import { Counter } from 'k6/metrics';
import { uuidv4 } from 'https://jslib.k6.io/k6-utils/1.0.0/index.js';

let VUsCount = __ENV.VUS ? __ENV.VUS : 5;
const vuInitTimeoutSecs = 5; // adjust this if you have a longer init!
const loadTestDurationSecs = 30;
const loadTestGracefulStopSecs = 5;

let vuSetupsDone = new Counter('vu_setups_done');

export let options = {
    scenarios: {
        // This is the per-VU setup/init equivalent:
        vu_setup: {
            executor: 'per-vu-iterations',
            vus: VUsCount,
            iterations: 1,
            maxDuration: `${vuInitTimeoutSecs}s`,
            gracefulStop: '0s',

            exec: 'vuSetup',
        },

        // You can have any type of executor here, or multiple ones, as long as
        // the number of VUs is the pre-initialized VUsCount above.
        my_api_test: {
            executor: 'constant-arrival-rate',
            startTime: `${vuInitTimeoutSecs}s`, // start only after the init is done
            preAllocatedVUs: VUsCount,

            rate: 5,
            timeUnit: '1s',
            duration: `${loadTestDurationSecs}s`,
            gracefulStop: `${loadTestGracefulStopSecs}s`,

            // Add extra tags to emitted metrics from this scenario. This way
            // our thresholds below can only be for them. We can also filter by
            // the `scenario:my_api_test` tag for that, but setting a custom tag
            // here allows us to set common thresholds for multi-scenario tests.
            tags: { type: 'loadtest' },
            exec: 'apiTest',
        },

        // This is the per-VU teardown/cleanup equivalent:
        vu_teardown: {
            executor: 'per-vu-iterations',
            startTime: `${vuInitTimeoutSecs + loadTestDurationSecs + loadTestGracefulStopSecs}s`,
            vus: VUsCount,
            iterations: 1,
            maxDuration: `${vuInitTimeoutSecs}s`,
            exec: 'vuTeardown',
        },
    },
    thresholds: {
        // Make sure all of the VUs finished their setup successfully, so we can
        // ensure that the load test won't continue with broken VU "setup" data
        'vu_setups_done': [{
            threshold: `count==${VUsCount}`,
            abortOnFail: true,
            delayAbortEval: `${vuInitTimeoutSecs}s`,
        }],
        // Also make sure all of the VU teardown calls finished uninterrupted:
        'iterations{scenario:vu_teardown}': [`count==${VUsCount}`],

        // Ignore HTTP requests from the VU setup or teardown here
        'http_req_duration{type:loadtest}': ['p(99)<300', 'p(99.9)<500', 'max<1000'],
    },
    summaryTrendStats: ['min', 'med', 'avg', 'p(90)', 'p(95)', 'p(99)', 'p(99.9)', 'max'],
};

let vuCrocName = uuidv4();
let httpReqParams = { headers: {} }; // token is set in init()

export function vuSetup() {
    vuSetupsDone.add(0); // workaround for https://github.com/loadimpact/k6/issues/1346

    let user = `croco${vuCrocName}`
    let pass = `pass${__VU}`

    let res = http.post('https://test-api.k6.io/user/register/', {
        first_name: 'Crocodile',
        last_name: vuCrocName,
        username: user,
        password: pass,
    });
    check(res, { 'Created user': (r) => r.status === 201 });

    // Add some bogus wait time to see how VU setup "timeouts" are handled, and
    // how these requests are not included in the http_req_duration threshold.
    let randDelay = Math.floor(Math.random() * 4)
    http.get(`https://httpbin.test.k6.io/delay/${randDelay}`);

    let loginRes = http.post(`https://test-api.k6.io/auth/token/login/`, {
        username: user,
        password: pass
    });

    let vuAuthToken = loginRes.json('access');
    if (check(vuAuthToken, { 'Logged in user': (t) => t !== '' })) {
        console.log(`VU ${__VU} was logged in with username ${user} and token ${vuAuthToken}`);

        // Set the data back in the global VU context:
        httpReqParams.headers['Authorization'] = `Bearer ${vuAuthToken}`;
        vuSetupsDone.add(1);
    }
}


export function apiTest() {
    const url = 'https://test-api.k6.io/my/crocodiles/';
    const payload = {
        name: `Name ${uuidv4()}`,
        sex: 'M',
        date_of_birth: '2001-01-01',
    };

    let newCrocResp = http.post(url, payload, httpReqParams);
    if (check(newCrocResp, { 'Croc created correctly': (r) => r.status === 201 })) {
        console.log(`[${__VU}] Created a new croc with id ${newCrocResp.json('id')}`);
    }

    let resp = http.get(url, httpReqParams);
    if (resp.status == 200 && resp.json().length > 3) {
        let data = resp.json();
        if (data.length > 3) {
            let id = data[0].id;
            console.log(`[${__VU}] We have ${data.length} crocs, so deleting the oldest one ${id}`);
            let r = http.del(`${url}/${id}/`, null, httpReqParams);
            check(newCrocResp, { 'Croc deleted correctly': (r) => r.status === 201 })
        }
    }
}


export function vuTeardown() {
    console.log(`VU ${__VU} (${vuCrocName}) is tearing itself down...`);

    // In the real world, that will be actual clean up code and fancy error
    // catching like in vuSetup() above. For the demo, you can increase the
    // bogus wait time below to see how VU teardown "timeouts" are handled.
    sleep(Math.random() * 5);

    console.log(`VU ${__VU} (${vuCrocName}) was torn down!`);
}
1 Like