API Performance Testing with K6

API Performance Testing with K6

Background

When I started running performance testing with Jmeter, I used to get very frustrated at the way the JVM will consume every single byte of memory on my machine. Whenever I needed to run a performance test at work I would request for 2 dedicated Virtual Machine with 128Gb RAM to run distributed load testing. I worked for one of top Nigeria Banks and we needed some serious performance tests serving up to 27 million customers on our omni-channel platforms.

When I got introduced to Go and programming and I saw how Go was very great with performance, the first idea that came to my mind was to build a replica of Jmeter using Go. I felt that would be an awesome tool to generate huge traffic with very little resources but I didn't know enough to be able to kick start then.

Interestingly , a Stockholm-based startup Load Impact did exactly that , they built an open source performance testing tool in Go and released it in 2017. It's called k6, it was acquired by Grafana Labs in 2021 and k6 is now part of Grafana Labs.

Why k6

  • A modern load testing tool built for developer happiness ( that's interesting, they only want to make developers happy...)

  • High Performance Tool

  • Test Integrations - You can integrate with several tool, run your tests in your ci/cd pipeline , report test to DataDog, Grafana etc.

  • Simple and Easy to use

It uses the command line, although many people who are familiar with Jmeter will prefer a UI. I will log a feature request for that, let's see if it's in the roadmap.

Before we go further , if you are completely new to k6 , I will recommend you take a quick peek at the documentation.

Install k6

I am using brew , but you may need to use something else depending on your OS/preference

brew install k6

Sample k6 test

import http from 'k6/http';
import { sleep } from 'k6';

export default function () {
  http.get('https://test.k6.io');
  sleep(1);
}

The above will send an http get request to the url , it's that simple. Let's get into more details for our task.

API Testing in K6

Task: I've been asked to run a performance test on an API. This API would require a JWT Bearer Token Authentication passed in the header to make successful calls.

Call Auth endpoint to get token and add token in the header parameter of subsequent calls.

A good approach here is to leverage the setup() and teardown() functions of k6 . I have created a file called script.js

Setup Script

import http from 'k6/http';

const baseURL = "https://carestationrest.herokuapp.com/api/v1/"

export const options = {
  vus: 1,
  duration: '1s',
};

const headerinfo = {
  headers: {
    'Content-Type': 'application/json'
  },
};

export function setup() {
console.log("setup function started")

const  loginParam = JSON.stringify({
    username: 'demouser',
    password: '12345678'
  })

  const loginURL = baseURL +'login'
  const res = http.post(loginURL , loginParam, headerinfo)

  const access_token = res.json().access_token 

  return { token: access_token };
}

Let me explain what's going on above

  1. Import k6 in my script
  2. Define baseURL and k6 options (no of VUs i.e virtual users and duration of run)
  3. Defined headerInfo to pass header parameters
  4. Created a setup function to call the token endpoint to grab a token and return it as a Json data. This function is called once at the beginning of the test. Within the function, we are using check to verify the response code is 200 Ok.

That's it for our setup function.

Main Test

Our main test will be in the exported default function and we can use the data from the setup().

import http from 'k6/http';
import { expect } from 'https://jslib.k6.io/k6chaijs/4.3.4.1/index.js';

const baseURL = "https://carestationrest.herokuapp.com/api/v1/"

export const options = {
  vus: 1,
  duration: '1s',
};

const headerinfo = {
  headers: {
    'Content-Type': 'application/json'
  },
};

export function setup() {
console.log("setup function started")

const  loginParam = JSON.stringify({
    username: 'demouser',
    password: '12345678'
  })

  const loginURL = baseURL +'login'
  const res = http.post(loginURL , loginParam, headerinfo)

  expect(res.status, 'response status').to.equal(200);

  const access_token = res.json().access_token 

  return { token: access_token };
}

Similar to the setup(), we are making the http post request, but you'll notice we passed in data as argument to our default function. This data contains the json output from our setup function. The setup functions sets up data for processing and able to share data among VUs.

We also modify the headers to Add another parameter called Authorization which contains the bearer token.

One last thing , we can add a report to generate a basic html report for us. We do this by importing the reporter and passing the test result into the reporter. Our function that handles reporting is called handleSummary.

Our final code becomes.

import http from 'k6/http';
import { htmlReport } from "https://raw.githubusercontent.com/benc-uk/k6-reporter/main/dist/bundle.js";
import { expect } from 'https://jslib.k6.io/k6chaijs/4.3.4.1/index.js';

const baseURL = "https://carestationrest.herokuapp.com/api/v1/"

export const options = {
  vus: 1,
  duration: '1s',
};

const headerParam = {
  headers: {
    'Content-Type': 'application/json'
  },
};

export function setup() {
console.log("setup function started")

const  loginParam = JSON.stringify({
    username: 'demouser',
    password: '12345678'
  })

  const loginURL = baseURL +'login'
  const res = http.post(loginURL , loginParam, headerParam)

  expect(res.status, 'response status').to.equal(200);

  const access_token = res.json().access_token 

  return { token: access_token };
}

export default function (data) {

  headerParam.headers.Authorization = 'Bearer ' + data.token

  const getUsersURL = baseURL + 'users'
  const res =  http.get(getUsersURL, headerParam);
   expect(res.status, 'response status').to.equal(200);

}


export function handleSummary(data) {
  return {
    "summary.html": htmlReport(data),
  };
}


export function teardown(data) {
  console.log("teardown function started")
}

NB: I have an empty teardown function , This is very important , you can use this function to clean up you test, re-init database or anything you would like to do to reset you test environment.

Execute Test
k6 run script.js

Screen Shot 2022-06-05 at 4.14.17 AM.png

That's it. You can check the this repo for the full source code.

Reports

Here is the main report showing number of request made in the entire test. In this case , I did only 1 sec , so it made 7 requests. 1 for the token and another for 6 for the actual test. You can see the average response time.

Screen Shot 2022-06-05 at 3.52.33 AM.png

Other metrics

You can see the hits per seconds.
Screen Shot 2022-06-05 at 3.52.43 AM.png

Checks

You can see the checks that we added and they all passed. Checks are important to verify your test are actually returning the expected result.

Screen Shot 2022-06-05 at 3.52.58 AM.png

I hope you enjoyed the tutorial. Drop your comments or questions for me and let me know if you need any help with your setup.

Connect with me on Linkedin.