Tutorials 17 October 2022

Using chai with k6

Ben Simpson

The growth of a code base is unpredictable. To account for this uncertainty, we call on everything we've learnt in the last x years about how to scale an application effectively: adopting naming conventions, creating file and folder structures, using the latest patterns, and producing sensible abstractions. These actions add up over time. Eventually, you'll mentally high-five your past self for having taken the time to do them ✋.

Your tests should be future-proof too. But, along with the typical development best practices, testing requires some unique strategies to scale with the application, all whilst making sure that the test itself is stable, predictable, and adds some value by providing confidence that it's working the way it should, not just raising your coverage score.

Out of the box, k6 provides the beloved check function. Yes, it does the job, but it does very little to organise our tests into a structure that we can navigate with ease, an absolute must for larger applications.

import { check } from 'k6';
import http from 'k6/http';
export default function () {
const res = http.get('http://test.k6.io/');
check(res, {
'is status 200': (r) => r.status === 200,
});
}

These days, most Javascript developers use test frameworks such as Jest, Jasmine, or Mocha and Chai. They enforce conventions that require the user to name their tests and provide human-readable expectations for the results. We wanted to offer that same level of familiarity, so we wrote something we think you might like.

A k6 library for ChaiJs

We would like to share that k6 has released k6chaijs! This library comes with Chai built-in and some extras to make it play nicely with k6.

Getting to know chai

Chai is a BDD / TDD assertion library for node and the browser that can be delightfully paired with any javascript testing framework.

If you're not familiar with the terms BDD and TDD, here is a quick summary.

Behavior Driven Development (BDD): The use of human-readable descriptions of software requirements as the basis for software tests.

Test-driven development (TDD): A process that creates software test cases before building the application, so the software can be iteratively developed until all test cases pass.

Reasons to use an assertion library

Expressive language

By writing tests in Chai, you can use expressive language to avoid code interpretation. Just think about having to read through hundreds of tests like this:

if ('Hello'.length !== 5) {
throw new Error(`Expected 'Hello' to have a length of 5 but got ${'Hello'.length}`);
}

How would you even find them!? 😰

Let's take a look at a simple test written using Chai. What do you think the following code expects?

import { expect, describe } from '...';
describe('should match expected length', () => {
expect('Hello').to.have.lengthOf(5);
});

Easy! It expects the string Hello to have a length of 5.

Automatic messages

Tired of writing test descriptions and error messages? Assertion libraries have you covered, with automatically generated messages!

import { expect, describe } from '...';
describe('should match expected length', () => {
// 🔥 AssertionError: expected 'Goodbye' to have a length of 6 but got 7
expect('Goodbye').to.have.lengthOf(6);
});

Structure

Through encapsulation, your tests can define sections for your application. This makes it easier to:

  • Locate tests
  • Spot missing edge cases
  • Keep everything tidy
  • Provide a logical format for adding additional tests.
import { describe } from '...';
describe('service A', () => {
describe('should do x from service A', () => {
/*...*/
});
describe('should do y from service A', () => {
/*...*/
});
});
describe('service B', () => {
describe('should do x from service B', () => {
/*...*/
});
describe('should do y from service B', () => {
/*...*/
});
});

Discoverability

Imagine an application with hundreds of tests and a mostly green test summary. "Mostly" green means it has some failures.

To track down these failures, you can copy the test name, search your test project for a match, and immediately narrow down which part of the application is failing.

However, for this to work, you need neatly structured tests and unique, descriptive test names.

summary

Flexibility

Although an assertion library is typically used to write mocked unit/integration tests, that doesn't mean we can't use it to test our API responses.

import { expect } from '...';
import http from '...';
const expected = { foo: 'Hello', bar: 'World' };
const response = http.get('https://your.example.domain');
expect(response.body).to.deep.equal(expected);

Stability

By itself, the k6 check function is not enough to protect your tests against unsafe code. When your system is under load it is possible for some requests to start failing.

import { check } from 'k6';
import http from 'k6/http';
export default function () {
const response = http.get('https://your.example.domain'); // 🙈 could return Error 503
check(response, {
'got more than 5 items': (res) => {
// 🙉 `.json()` might be undefined
return res.json().length > 5;
},
}); // 🙊 k6 will throw an exception and restarts execution from the beginning.
// 💀 RIP
check(response, {
/*...*/
});
}

Fortunately, with k6chaijs error handling is provided automatically. Script errors are caught by the describe block and execution can proceed to the next set of tests.

import { describe, expect } from 'k6';
import http from 'k6/http';
export default function () {
// 😇 You are safe now
describe('got more than 5 items', () => {
const response = http.get('https://your.example.domain'); // 🙈 could return Error 503
// 🙉 `.json()` might be undefined
expect(response.json()).to.have.lengthOf(5);
});
describe('hooray I still get a turn!', () => {
/*...*/
});
}

Using k6 with chai

Let's look at an example script and its end-of-test summary.

The script

The following script incorporates thresholds with 3 test cases that use Chai:

  • One describe function creates sections and groups tests together
  • Another describe provides clear instructions on what we hope to achieve from each test
  • Chai's BDD-style expect function is used to write the tests in an expressive, readable way
import { describe, expect } from 'https://jslib.k6.io/k6chaijs/4.3.4.2/index.js';
import http from 'k6/http';
export default function () {
describe('crocodiles API', () => {
describe('should fetch a list of public crocodiles', () => {
const response = http.get('https://test-api.k6.io/public/crocodiles');
expect(response.status, 'response status').to.equal(200);
expect(response).to.have.validJsonBody();
expect(response.json().length, 'number of crocs').to.be.above(4);
});
describe('should respond with status 200, when a valid user id is provided', () => {
const expected = {
id: 6,
name: 'Sang Buaya',
sex: 'F',
date_of_birth: '2006-01-28',
age: 16,
};
const response = http.get('https://test-api.k6.io/public/crocodiles/6');
expect(response.status, 'status').to.equal(200);
expect(JSON.parse(response.body), 'response body').to.deep.equal(expected);
});
describe('should respond with status 404, when an invalid user id is provided', () => {
const response = http.get('https://test-api.k6.io/public/crocodiles/9999999');
expect(response.status, 'status').to.equal(404);
expect(JSON.parse(response.body).detail, 'error message').to.equal('Not found.');
});
});
}

Under the hood

If you were wondering how this all works, the explanation is pretty straightforward.

describe

k6 users will use the group function to perform this type of operation, however, for users of Javascript testing frameworks, describe is the familiar term. It still calls group, but it's wrapped with a little bit of extra logic.

expect

As you might have already guessed, Chai's expect function has been modified to include some k6 goodness with the eventual execution of the check function.

Why am I telling you all this? Well, because we're leaning on the k6 functions group and check to categorise and test our APIs.

k6 users benefit from using a powerful assertion library, but they'll still get the output summaries they know and love ❤️.

The summary

Let's see how all this hard (well, not really) work translates to the output summary:

  • All tests are grouped nicely together, with clear descriptions
  • Each assertion is clearly labelled
  • Each assertion has passed, so the overall test has succeeded!

summary

For cloud users, the results are going to display just as they normally would, under the checks tab.

cloud-checks-tab

Achievement unlocked 🥳

We've seen how an assertion library like Chai with k6 can make reading and writing tests more manageable, which becomes increasingly important as an application scales. It also makes tests easier to comprehend for everyone, not just testers and engineers.

Future plans

  • Release as an npm package.
  • Provide more ways to structure tests.
  • Create additional custom assertions.

Get involved

If you're interested in contributing, you can find k6chaijs on Github. We hope you enjoy using this library and look forward to receiving your feedback!

< Back to all posts