Understanding the behavior of software under high traffic is crucial for understanding the impact of changes to your system and making informed optimization decisions.

Load testing is a non-functional test that enables developers and Q/A teams to understand the behavior of an application under load by simulation. This is extremely useful for testing changes before deploying them to production. In this tutorial, we’ll look at K6, a tool developed by Grafana Labs that enables users to write HTTP and GRPC APIs using supported programming languages.

As a tool developed by Grafana Labs, K6 integrates seamlessly with Grafana, a popular open-source data visualization and monitoring platform. This integration lets you visualize and analyze load test results in real-time. In addition, K6 uses JavaScript (ES6 syntax specifically) for writing test scripts, making it easy for developers to learn.

In practical scenarios, K6s can be used in an e-commerce platform to simulate a Black Friday sale event with a high number of virtual users. This helps them identify potential bottlenecks in their system before the sale, preventing slowdowns and lost revenue.

Prerequisites

This tutorial assumes some familiarity with Kubernetes as well as a basic understanding of javascript, In addition, you would need the following installed locally:

Creating a Kubernetes Cluster

We’ll begin by creating a Kubernetes cluster, feel free to skip this step if you have a cluster created already.

For simplicity, we will be doing it from the CLI ↓

civo k3s create --create-firewall --nodes 2 -m --save --switch --wait k6-demo

This will launch a two-node cluster and point your kube-context to the cluster we just launched.

Installing K6

K6 is written in Golang and can be installed using the standalone binary or using the docker image; for Mac users, you can install K6 using brew ↓

brew install k6

For Linux users on Debian-based distros, you can install K6 using ↓

sudo gpg -k
sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list
sudo apt-get update
sudo apt-get install k6

At the time of writing, this will install version v0.50.0 of K6. You can verify the installation was successful by running the following command ↓

k6 --version

The output is similar to ↓

k6 v0.50.0 (go1.22.1, darwin/arm64)

Writing HTTP Tests

Through its SDK, K6 allows users to write tests for APIs and browser-based applications; for this demonstration, we will write a test for an HTTP service. We’ll begin by creating a deployment for the HTTP service.

Create a Deployment & Apply Manifest

In a directory of your choice, create a file called deployment.yaml and add the following code ↓

apiVersion: apps/v1
kind: Deployment
metadata:
  name: whoami-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
    app: whoami
  template:
    metadata:
    labels:
        app: whoami
    spec:
    containers:
    - name: whoami
        image: traefik/whoami
        ports:
        - containerPort: 80
        name: web
---
apiVersion: v1
kind: Service
metadata:
  name: whoami-service
spec:
  selector:
    app: whoami
  ports:
  - protocol: TCP
    port: 80
    targetPort: web

The manifest above creates a single replica of the whoami server and exposes the deployment on port 80.

Writing a Load Test

K6 test scripts use JavaScript, while K6 is written in Golang. Using a package called goja, an implementation of ECMAScript 5.1 in pure Go, K6 can execute JavaScript while maintaining performance.

We’ll begin by creating a suite of tests to evaluate different parts of the service we deployed earlier.

Create a file called load.js and add the following code ↓

import http from 'k6/http';
import { check, group } from 'k6';


const BASE_URL = 'http://localhost:8080';


export default function () {

 group('health check test (POST 500)', () => {

  // set status of response too 500
   http.post(`${BASE_URL}/health`, "500");
 
   const res =  http.get(`${BASE_URL}/health`);
    
    check(res, {
    'is status 500': (r) => r.status === 500,

    });
  });

  group('response time test', () => {
   const res = http.get(`${BASE_URL}/?wait=100ms`);
    check(res, {

    'check request duration': (r) => res.timings.duration <= 5000,
    });
  });

  group('status code test', () => {
  const  res =  http.get(`${BASE_URL}/ip`)

    check(res, {
    'verify homepage text': (r) =>

        r.body.includes('ip'),
 
    });
  });

}

Here’s a breakdown of what’s going on:

Using the group function from K6 to structure related tests. Each group has a descriptive name enclosed in quotes, like "health check test (POST 500)".

The check function ensures the HTTP requests return the expected results. It takes two arguments: the response object (res) and an object containing assertions (checks).

r.body.includes('ip') verifies if the response body (r.body) of the request to /ip includes the word "ip". This type of check is particularly useful for validating if the response contains expected text.

Running the Test

Before executing the test script, we need to expose the application we deployed earlier, for simplicity, we would be exposing the application on port 8080 using Kubectl ↓

kubectl port-forward svc/whoami-service 8080:80

With the tests written, we can execute the code using the run sub-command. Within the directory, run the following command in your test file ↓

k6 run load.js

Output:

Load Testing with K6

In the test run above, all three groups within the test. The test received a total of 852 bytes of data at an average rate of 179 bytes per second, on average, each test iteration took 1.58 seconds to complete. The fastest iteration finished in 362 milliseconds, while the slowest took 3.74 seconds. 90% of iterations completed within 3.12 seconds, and 95% within 3.43 seconds.

Clean up (Optional)

After completing this tutorial, you may want to clean up the resources you created. To delete the Kubernetes cluster, run the following command ↓

civo k3s delete k6-demo

This command will delete the K6-demo cluster from your Civo account.

Summary

Load testing can be extremely useful for testing the impact of small and significant changes to the software; combining K6s portability with your continuous integration of choice makes it a compelling choice for continuously testing changes in your applications.

If you’re looking to take your K6 scripting further, here are some ideas: