DEV Community

Cover image for API Performance Testing with k6 - A Quick Start Guide
nadirbasalamah
nadirbasalamah

Posted on

API Performance Testing with k6 - A Quick Start Guide

Introduction

The definition of a high-quality REST API application is an application that can operate well and perform well. The performance of the REST API can be measured by performance testing to ensure the application is operational, scalable, and reliable on many different workloads. From normal workloads to unexpected heavy workloads.

Why Performance Testing?

The performance testing provides some benefits during REST API development:

  • Ensuring the performance of the REST API meets the desired standards, especially in unexpected workloads.

  • Detecting performance bottlenecks earlier before it went to production.

What is k6?

Many tools can be used for performance testing. One of those is k6. k6 is a performance testing tool developed by Grafana Labs for conducting performance testing in various platforms like REST API and web applications. The k6 is a code-based tool which means the testing script is written in a Javascript code. The k6 can be utilized even more by using additional plugins and extensions.

k6 Setup

These are the required steps for using k6:

  1. Install the k6. The k6 is available in Docker container and binary. Make sure to install it based on the operating system that is being used.

  2. Configure the code editor. The code editor needs to be configured to enable the IntelliSense feature to enhance the developer experience when writing testing scripts.

Writing the First Test

The k6 testing scripts can be organized into one project because when working in a project, the scripts can be organized easily.

Create the npm project by running this command.

npm init --yes
Enter fullscreen mode Exit fullscreen mode

If you want to specify the additional information of the project, use npm init.

After that, install the k6 types for enabling IntelliSense feature.

npm install --save-dev @types/k6
Enter fullscreen mode Exit fullscreen mode

The test script can be generated automatically using k6 new command. This is the structure of the command.

k6 new <directory_name>/filename.js
Enter fullscreen mode Exit fullscreen mode

make sure the directory is exists.

If the directory location is not specified. This command creates a test script in the current working directory. The test script can be created manually as well.

This is the example of creating a new test script in the src directory.

k6 new src/test.js
Enter fullscreen mode Exit fullscreen mode

This is the content of the created test script. The comments inside the test script is already removed.

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

export const options = {
  // A number specifying the number of VUs to run concurrently.
  vus: 10,
  // A string specifying the total duration of the test run.
  duration: "30s",
};

export default function () {
  http.get("https://test.k6.io");
  sleep(1);
}
Enter fullscreen mode Exit fullscreen mode

The test can be executed using k6 run command followed by the file name.

k6 run src/test.js
Enter fullscreen mode Exit fullscreen mode

This is the output of the test.

k6 Test Result

After the test is executed, the k6 generates a summary output that describes information like the number of successful requests, duration of the requests, and others. This is a detailed explanation of the output summary.

Standard built-in metrics

Metric name Description
checks The rate of successful checks
data_received The amount of received data
data_send The amount of sent data
iteration_duration The amount of time to complete a single iteration
iterations The aggregate number of times the VUs execute the test script (the default function)
vus The current number of active virtual users
vus_max The maximum potential number of virtual users

HTTP-specific built-in metrics

Metric name Description
http_req_blocked Time to spent waiting for a free TCP connection slot before initiating the request
http_req_connecting The duration of establishing a TCP connection to the remote host
http_req_duration The total duration of the request
http_req_failed The rate of failed requests according to setResponseCallback
http_req_receiving The duration of receiving response data from the remote host
http_req_sending The duration of sending data to the remote host
http_req_tls_handshaking The duration of establishing TLS handshake session with remote host
http_req_waiting The duration of waiting for the response from the remote host
http_reqs The total amount of generated HTTP requests by k6

Learn more about the k6 metrics here.

Load Testing Types

There are many types of load testing for measuring the performance of the system. Each type serves different purposes and requirements.

Smoke Testing

The smoke testing ensures the system is operational with a minimal load and verifies the test scripts. The smoke testing must be executed with a small number of virtual users (from 2 up to 5 VUs) and short duration.

This is an example of smoke testing for getting all posts feature.

import http from "k6/http";
import { check } from "k6";

export const options = {
  vus: 3, // 3 virtual users
  duration: "40s", // duration is 40 seconds
};

export default function () {
  // sends GET request
  const response = http.get("https://jsonplaceholder.typicode.com/posts");

  // validate the response
  check(response, {
    "is status 200": (r) => r.status === 200,
    "is not null": (r) => r.json() !== null,
  });
}
Enter fullscreen mode Exit fullscreen mode

Average Load Testing

The average load testing ensures the system is operational with typical loads. Typical loads mean the workload during the regular day in a production environment.

This is the example of average load testing for getting all posts feature.

import http from "k6/http";
import { check } from "k6";

export const options = {
  stages: [
    { duration: "5m", target: 100 }, // traffic ramp-up from 1 to 100 users over 5 minutes.
    { duration: "30m", target: 100 }, // stay at 100 users for 30 minutes
    { duration: "5m", target: 0 }, // ramp-down to 0 users
  ],
};

// function for generating random string
const generateRandomString = (start, end) => {
  const res = Math.random().toString(36).substring(start, end);
  return res;
};

// function for generating random ID
const generateRandomId = () => {
  const id = Math.floor(Math.random() * 1000);
  return id;
};

export default function () {
  // prepare request body
  const requestBody = {
    title: generateRandomString(1, 7),
    body: generateRandomString(1, 20),
    userId: generateRandomId(),
  };

  // sends POST request
  const response = http.post(
    "https://jsonplaceholder.typicode.com/posts",
    JSON.stringify(requestBody),
    {
      headers: {
        "Content-Type": "application/json",
        Accept: "application/json",
      },
    }
  );

  // validate the response
  check(response, {
    "is status 201": (r) => r.status === 201,
    "is not null": (r) => r.json() !== null,
    "is contains valid id": (r) => r.json().id > 0,
    "is contains valid title": (r) => r.json().title !== "",
    "is contains valid body": (r) => r.json().body !== "",
  });
}
Enter fullscreen mode Exit fullscreen mode

Stress Testing

The stress testing ensures the system is operational with heavier loads than usual. The stress testing verifies the stability and reliability of the system under heavy workloads. The system may receive heavy workloads in many moments such as flash-sale, payday, school or university admission process, and other similar moments. When conducting the stress testing, the number of workloads must be heavier than typical workloads and must be executed after average load testing.

This is an example of stress testing for creating a new post feature.

import http from "k6/http";
import { check } from "k6";

export const options = {
  stages: [
    { duration: "10m", target: 200 }, // traffic ramp-up from 1 to a higher 200 users over 10 minutes.
    { duration: "30m", target: 200 }, // stay at higher 200 users for 30 minutes
    { duration: "5m", target: 0 }, // ramp-down to 0 users
  ],
};

// function for generating random string
const generateRandomString = (start, end) => {
  const res = Math.random().toString(36).substring(start, end);
  return res;
};

// function for generating random ID
const generateRandomId = () => {
  const id = Math.floor(Math.random() * 1000);
  return id;
};

export default function () {
  // prepare request body
  const requestBody = {
    title: generateRandomString(1, 7),
    body: generateRandomString(1, 20),
    userId: generateRandomId(),
  };

  // sends POST request
  const response = http.post(
    "https://jsonplaceholder.typicode.com/posts",
    JSON.stringify(requestBody),
    {
      headers: {
        "Content-Type": "application/json",
        Accept: "application/json",
      },
    }
  );

  // validate the response
  check(response, {
    "is status 201": (r) => r.status === 201,
    "is not null": (r) => r.json() !== null,
    "is contains valid id": (r) => r.json().id > 0,
    "is contains valid title": (r) => r.json().title !== "",
    "is contains valid body": (r) => r.json().body !== "",
  });
}
Enter fullscreen mode Exit fullscreen mode

Spike Testing

The spike testing ensures the system is operational with suddenly heavy workloads. Examples of sudden heavy workloads are product launch events, limited-time discount,s and others. During spike testing, the heavy workloads increased suddenly without any ramp-down or "pause time".

This is an example of spike testing for updating a post feature.

import http from "k6/http";
import { check } from "k6";

export const options = {
  stages: [
    { duration: "2m", target: 500 }, // fast ramp-up without any break
    { duration: "1m", target: 0 }, // quick ramp-down
  ],
};

// function for generating random string
const generateRandomString = (start, end) => {
  const res = Math.random().toString(36).substring(start, end);
  return res;
};

// function for generating random ID
const generateRandomId = () => {
  const id = Math.floor(Math.random() * 100);
  return id;
};

export default function () {
  const sampleId = generateRandomId();

  // prepare request body
  const requestBody = {
    title: generateRandomString(1, 7),
    body: generateRandomString(1, 20),
    userId: sampleId,
  };

  // sends PUT request
  const response = http.put(
    `https://jsonplaceholder.typicode.com/posts/${sampleId}`,
    JSON.stringify(requestBody),
    {
      headers: {
        "Content-Type": "application/json",
        Accept: "application/json",
      },
    }
  );

  // validate the response
  check(response, {
    "is status 200": (r) => r.status === 200,
    "is not null": (r) => r.json() !== null,
    "is contains valid id": (r) => r.json().id > 0,
    "is contains valid title": (r) => r.json().title !== "",
    "is contains valid body": (r) => r.json().body !== "",
  });
}
Enter fullscreen mode Exit fullscreen mode

Endurance Testing

The endurance testing (also known as soak testing) ensures the system is operational with average loads for a long duration. This test measures the performance degradation and the availability and stability during long periods.

The endurance testing can be executed after the smoke and average load tests are executed. The duration of this test must be longer than any other kind of test.

Actually, the typical duration is in hours like 3,4,12, and up to 72 hours. For this example, the duration is reduced.

This is an example of endurance testing for creating a post feature.

import http from "k6/http";
import { check } from "k6";

export const options = {
  stages: [
    { duration: "10m", target: 100 }, // traffic ramp-up from 1 to 100 users over 10 minutes.
    { duration: "30m", target: 100 }, // stay at 100 users for 30 minutes
    { duration: "10m", target: 0 }, // ramp-down to 0 users
  ],
};

// function for generating random string
const generateRandomString = (start, end) => {
  const res = Math.random().toString(36).substring(start, end);
  return res;
};

// function for generating random ID
const generateRandomId = () => {
  const id = Math.floor(Math.random() * 1000);
  return id;
};

export default function () {
  // prepare request body
  const requestBody = {
    title: generateRandomString(1, 7),
    body: generateRandomString(1, 20),
    userId: generateRandomId(),
  };

  // sends POST request
  const response = http.post(
    "https://jsonplaceholder.typicode.com/posts",
    JSON.stringify(requestBody),
    {
      headers: {
        "Content-Type": "application/json",
        Accept: "application/json",
      },
    }
  );

  // validate the response
  check(response, {
    "is status 201": (r) => r.status === 201,
    "is not null": (r) => r.json() !== null,
    "is contains valid id": (r) => r.json().id > 0,
    "is contains valid title": (r) => r.json().title !== "",
    "is contains valid body": (r) => r.json().body !== "",
  });
}
Enter fullscreen mode Exit fullscreen mode

Using k6 Additional Libraries

The k6 provides additional Javascript libraries for enhancing testing processes like additional utilities, assertions, and others.

Assertion with k6chaijs

The assertion is a process to validate the response. The assertion is done by comparing the expected and the actual value. The assertion process can be done easily and comprehensively using k6chaijs. This library enables chai style assertion in the k6 test script.

In this example, the assertion is implemented using k6chaijs for testing the create post feature.

import http from "k6/http";
import {
  describe,
  expect,
} from "https://jslib.k6.io/k6chaijs/4.5.0.1/index.js";
import {
  randomIntBetween,
  randomString,
} from "https://jslib.k6.io/k6-utils/1.4.0/index.js";

export const options = {
  vus: 10,
  duration: "30s",
};

export default function () {
  describe("Create a new post", () => {
    // prepare request body
    const requestBody = {
      title: randomString(20),
      body: randomString(200),
      userId: randomIntBetween(1, 99),
    };

    // sends POST request
    const response = http.post(
      "https://jsonplaceholder.typicode.com/posts",
      JSON.stringify(requestBody),
      {
        headers: {
          "Content-Type": "application/json",
          Accept: "application/json",
        },
      }
    );

    // convert to JSON
    const jsonResponse = response.json();

    // perform assertions with k6chaijs
    expect(response.status, "response status").to.equal(201);
    expect(response).to.have.validJsonBody();
    expect(jsonResponse.id, "post ID").to.be.above(0);
    expect(jsonResponse.title, "title").to.not.equal(null);
    expect(jsonResponse.body, "body").to.not.equal(null);
    expect(jsonResponse.userId, "user ID").to.be.above(0);
  });
}
Enter fullscreen mode Exit fullscreen mode

Data-driven Test with Papaparse

The data-driven test in k6 can be implemented using Papaparse. Papaparse is a library for reading and processing CSV files.

In this example, the data-driven test is implemented for testing the update post feature.

The sample CSV file is available in the Github repository.

First, create an additional helper for retrieving random posts inside the random.js file in the helpers directory. The helpers directory is created inside the src directory.

export function getRandomPost(csvData) {
  let randIndex = getRandomInt(0, csvData.length);
  return {
    postData: csvData[randIndex],
    id: randIndex,
  };
}

function getRandomInt(min, max) {
  min = Math.ceil(min);
  max = Math.floor(max);
  return Math.floor(Math.random() * (max - min)) + min;
}
Enter fullscreen mode Exit fullscreen mode

Then, create a test script for testing the update post feature. In this script, the Papaparse library is utilized for reading the CSV file enabling data-driven testing.

import http from "k6/http";
import { SharedArray } from "k6/data";
import papaparse from "https://jslib.k6.io/papaparse/5.1.1/index.js";
import {
  describe,
  expect,
} from "https://jslib.k6.io/k6chaijs/4.3.4.3/index.js";
import { getRandomPost } from "./helper/random.js";

export const options = {
  vus: 20,
  duration: "30s",
};

const csvData = new SharedArray("sample user dataset", () => {
  // load CSV file
  return papaparse.parse(open("../resources/posts.csv"), { header: true }).data;
});

export default function () {
  describe("Update a post", () => {
    // get random post data from CSV
    const { postData, id } = getRandomPost(csvData);

    // prepare request body
    const requestBody = {
      title: postData.title,
      body: postData.body,
      userId: postData.userId,
    };

    // sends PUT request
    const response = http.put(
      `https://jsonplaceholder.typicode.com/posts/${id}`,
      JSON.stringify(requestBody),
      {
        headers: {
          "Content-Type": "application/json",
          Accept: "application/json",
        },
      }
    );

    const jsonResponse = response.json();

    // perform assertions with k6chaijs
    expect(response.status, "response status").to.equal(200);
    expect(response).to.have.validJsonBody();
    expect(jsonResponse.id, "post ID").to.be.above(0);
    expect(jsonResponse.title, "title").to.not.equal(null);
    expect(jsonResponse.body, "body").to.not.equal(null);
    expect(jsonResponse.userId, "user ID").to.be.above(0);
  });
}
Enter fullscreen mode Exit fullscreen mode

Resources

I hope this article helps you to learn about API performance testing using k6.

Do you have any experience working on performance testing using k6? Please let me know in the comments down below 👇

Thank you

Top comments (0)