Setup and Teardown running by Virtual User?

From the documentation i found that the exported function of Setup and Teardown run only once independently of how many VU (virtual users) we have.

On my case i want to do a workflow that goes like this:
I will do a test with many VU’s and for each VU do
1 time - Login a random user and Open a cashbook
n times - Do many requests which will repeat n times
1 time - Close cashbook

For my case it would be useful if i could somehow have something like a Setup and Teardown that is done by each VU. Is there a way to do something like this?

Hi there,

unfortunately there’s currently no easy way to accomplish this. You can follow issue #785 to be notified when this is improved, and in the meantime you can try some of the workarounds mentioned there. See this comment and this comment for details of both approaches. You might use __ITER this way for teardown as well, though it will be awkward.

This will likely be made easier after the upcoming v0.27.0 release, but no ETAs yet :slight_smile:

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

@ned this example cannot be applied for constant-vus executor right? Constant VUs
because it is spawning new VUs instead of reusing old ones (there is no preAllocatedVUs for that constant-vus).

So I need to do some if at the beginning of the constant-vus’s exec function and check if the VU has already been initialized, right?

Hi @grafbumsdi,

constant-vus doesn’t have preAllocatedVus key because … it can’t allocate them after it starts, so it just has vus which has the same function, but in the case of constant-arrival-rate (and ramping-arrival-rate) you can have 10 VUs in the beginning but need to allocate more.

In short, the hack above should work without problems for you.

But if you had an arrival-rate executor with preAllocatedVUs and maxVUs you will need to either use an if as you say or have to set the vu_setup’s vus to the maxVUs which practically negates the idea of having preAllocatedVUs and maxVUs having different values - only creating more VUs if you actually need them.

Hope this helps and that I haven’t confused you more :slight_smile:

1 Like