How to modularize your k6 tests

When it comes to software development, structuring your code in modules allow you to easily maintain and update the code when needed. As your load testing scenarios get more complicated and grow, you need to modularize your k6 tests so that you can reuse the code, as well as allowing the tests to be readable to other testers and developers.

Donald Le
Automation with Love

--

Photo by William Warby on Unsplash

If you have experience working with functional testing, such as end-to-end testing for web application using Selenium or Cypress, or with API testing using RestAssured in Java, you need to treat your test code the same way as application code. That means you need to structure your test code so that you can easily add new tests or modify the existing one when needed. That way, you can maintain the tests better and do not go to the point when you want to get rid of all your tests and build the new test project instead.

Photo by Lavi Perchik on Unsplash

The same goes for the performance testing using k6. To make your load tests to be maintainable, you need to modularize your test scenarios. k6 supports a number of methods to modularize your test components, from scenarios, thresholds, to the logic. Actually, the k6 team already provided a getting started tutorial with example for how to reuse and re-run tests by implementing the tests into modules.

In this article, I would like to share my approach when it comes to making the k6 tests to be modularized.

Thresholds

In the k6 testing, we usually need to verify whether our load test is passed or failed, based on certain requirement. Below is an example of making the thresholds to be reusable by creating a Javascript function with k6 supported methods:

export function threshold(scenarioName, expected_http_req_failed_rate, expected_http_req_duration_time){

switch (scenarioName)
{
case "constantUsers":
return {
http_req_failed: [{ threshold: expected_http_req_failed_rate}],
http_req_duration: [expected_http_req_duration_time],
}
case "checkNumberOfCCUs":
return {
http_req_failed: [{ threshold: expected_http_req_failed_rate, abortOnFail: true }],
}
default:
return {
http_req_failed: [{ threshold: expected_http_req_failed_rate}],
http_req_duration: [expected_http_req_duration_time],
}
}

}

This function takes scenarioName as a parameter so that you can apply different scenarios for your threshold. Currently, you can have different types of thresholds checking for 2 scenarios: constantUsers and checkNumberOfCCUs .

Scenarios

Scenarios definition are the most common part of your tests that you want to make it modularized so that you can run the same tests with different scenarios. One quick way to make your scenario definition maintainable is to creating a function for it. For example with this one below:

export function scenarios(scenarioName){
switch (scenarioName)
{
case "constantUsers":
return {
constantVirtualUsers: {
executor: 'constant-vus',
vus: 5,
duration: '1800s',
},
}
case "checkNumberOfCCUs":
return {
breaking: {
executor: "ramping-vus",
stages: [
{ duration: "10s", target: 2 },
{ duration: "150s", target: 5 },
{ duration: "150s", target: 10 },
{ duration: "150s", target: 15 },
{ duration: "150s", target: 20 },
{ duration: "150s", target: 25 },
{ duration: "150s", target: 30 },
{ duration: "150s", target: 35 },
{ duration: "150s", target: 40 },
{ duration: "150s", target: 45 },
{ duration: "150s", target: 50 },
{ duration: "150s", target: 55 },
{ duration: "150s", target: 60 },
{ duration: "150s", target: 65 },
{ duration: "150s", target: 70 },
{ duration: "150s", target: 75 },
{ duration: "150s", target: 80 },
{ duration: "150s", target: 85 },
{ duration: "150s", target: 90 },
],
},
}
default:
return {
constantVirtualUsers: {
executor: 'constant-vus',
vus: 5,
duration: '1800s',
},
}
}

};

By using this scenario function in your tests, you can apply different types of scenarios for your same test code, just by providing the scenarioName for your test, whether it is constantUsers or checkNumberOfCCUs. If the scenario name is not provided, the default scenario will be applied instead.

Request Functions

When applying the load testing with k6, you often need to deal with a number of API requests in your tests. To easily reuse the API requests, you need to create functions for every API, and put those functions into appropriate files that match their purposes. For example, below is the function for the authentication API:

export async function authentication(dataEnv, userLength){
const randomUser = randomNumber(0, userLength-1);
let POST_BODY = {
grant_type: 'password',
username: dataEnv.qa[randomUser].username,
password: dataEnv.qa[randomUser].password,
audience: dataEnv.authO.auth0Audience,
scope: 'openid profile email',
client_id: dataEnv.authO.auth0ClientId,
client_secret: dataEnv.authO.clientSecret,
};

let url = dataEnv.authO.auth0URL;
let res = http.asyncRequest('POST', url, JSON.stringify(POST_BODY), {
headers: { 'Content-Type': 'application/json' },
});
return res;
}

Test Data

Structing your test data in a way so that you can reuse it for different types of environment for your test is very important because you do not want to create different tests only because you want to execute the tests for another environment.

For example, you can store your test data in a Javascript variable with Json content for multiple environment as below:

export let obData = {
"dev":{
"client1":{
"a": null,
"b": "12",
"c": "12",
}
},
"test":{
"client1":{
"a": null,
"b": "13",
"c": "13",
}
},
};

Then you can use it in your test file for different environment test execution.

    let response = await createOb(dataEnv,tenant, obData, dataEnv.qa[1].apiKey);

Common Utilities

When doing the tests, there are common utility functions that you usually need to work with over and over again, such as random number generator function. Those common functions should be put in the common utility file so that you can reuse them in your tests.

For example below is a common function for generating random number:

export function randomNumber(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
}

Conclusion

Making the k6 tests modularized allow you to maintain the test project better and more efficiently. Through the article, I mentioned 5 components that you should modularize that are thresholds, scenarios, requests, test data, and common utilities.

If you would like to learn more about the best practices of using k6, check out the k6 official documentation page.

Happy k6 testing guys~~~

--

--

Donald Le
Automation with Love

A passionate automation engineer who strongly believes in “A man can do anything he wants if he puts in the work”.