Many of the multi-faceted applications development teams deploy every day are loosely coupled and every service exists to power another service. Most teams developing fullstack applications know that testing the communication between these services essential. Part of the process is testing HTTP request endpoints, and this tutorial focuses on exactly that. I will lead you through learning how to extend the k6 framework to test our HTTP endpoints. You will also learn how to set up k6 and and make use of features not found in any other API testing frameworks.

Prerequisites

To follow this tutorial, you will need:

  1. Basic knowledge of JavaScript
  2. Basic knowledge of HTTP requests and testing them
  3. Node.js (version >= 10.13) installed on your system
  4. A CircleCI account
  5. A GitHub account

Our tutorials are platform-agnostic, but use CircleCI as an example. If you don’t have a CircleCI account, sign up for a free one here.

What is k6 ?

k6 is an open source framework built to make performance testing for developers fun. At this point, you are probably asking yourself what this have to do with HTTP request testing? Testing performance means making requests to the API endpoints. We are already using the tool to make HTTP requests, why not use it to test those endpoints? Making the request takes you halfway to testing the endpoints. I will describe how to complete the test later in this tutorial.

k6 Logo

k6 is written in goja language, which is an Ecmascript 5.1 implementation of pure Go language. The implementation is purely for performance reasons and is packed with great features that make testing easy for software developers with JavaScript experience. k6 comes bundled with a useful CLI for making k6 commands that are supported by APIs. The k6 APIs offer support for JavaScript ES2015/ES6, and tools and capabilities for load testing, which we will use in a follow up to this tutorial.

Now that we know what runs under the k6 hood, why not dive into setting up k6 to run our HTTP tests?

First, clone the sample application from the Github repository.

Installing k6

Unlike JavaScript modules, k6 must be installed by use of a package manager. In macOS we can use Homebrew, while in Windows OS, we can use chocolatey. There are more installation options available for other operating systems in the k6 documentation. For this tutorial we will follow the macOS installation guide, and to do this, we will first execute the following command in homebrew:

brew install k6

This is the only command we need to run to get started with writing k6 tests.

After installing k6, we will need to create a folder to store our project. We will do this by executing the following command in the directory where we want to store our tests:

$ mkdir http-request-testing-with-k6;
$ touch todos-testing.js

These commands will create an empty project and also create our test file, todos-testing.js.

k6 Test structure

k6 utilizes a test structure that is a bit different from what most developers are used to, especially those coming from a JavaScript background. The code snippet below shows a sample test written in k6, asserting that we are getting a response from an open source todo app API that we will use for the purpose of testing in this tutorial. You can add the same test to the project that you just created.

// snippet from ./todos-testing.js
import http from 'k6/http';
import { check } from 'k6';
 
export let options = {
   vus: 1,
};
 
export default function () {
   group('API uptime check', () => {
       const response = http.get('https://todo-app-barkend.herokuapp.com/todos/');
       check(response, {
           "status code should be 200": res => res.status === 200,
       });
   })

First things first, if you carefully look at the test, it resembles JavaScript test code, but the file format is different from how we would normally write JavaScript tests. Let’s break down the test structure.

In the above test, since we learned that k6 uses goja and not JavaScript, we are able to write JavaScript code, but we are only able to use some aspects of JavaScript to execute our tests. Therefore, we do not have access to methods like describe or it blocks that we normally have access to, nor do we have access to assertion libraries like chai. k6 provides group() and check() methods that work the same way we would use describe and assert. The group() block allows us to write multiple tests in a single file but also separate the configurations of the different tests for ease of readability.

k6 also comes bundled with a default HTTP request module that we can use to make all our API requests, whether GET POST, PUT, or DELETE. This http() method would be the equivalent of fetch() when writing tests in pure JavaScript. For the rest of the test, we assert using the check() method that the response is 200, and we can go ahead and assert any other responses we need using this assertion block.

Tip: The options = {} object in the k6 test is optional for running HTTP tests, but it is very handy when we are running load tests with k6. In this particular test, we are telling k6 that we want to run the test with only a single virtual user (vus) or a single iteration, which makes sense for the context of this test.

Lifecycle of a k6 test

k6 follows a lifecycle of init code, setup code, VU code and teardown code. This cycle shows how a test would normally run and which resources the test has access to while it is executing. Here is a sample code block showing a k6 test lifecycle:

// 1. init code

export function setup() {
  // 2. setup code
}

export default function (data) {
  // 3. VU code
}

export function teardown(data) {
  // 4. teardown code
}

The init and VU stages serve as the test entry point, while teardown and setup provide scripts that the API tests can run before and after the tests have completed executing. The teardown and setup methods are optional, so the simplest form of a test in k6 could use only the VU code method, shown in this code snippet:

// snippet from ./todos-testing.js
export default function () {
    const response = http.get('https://todo-app-barkend.herokuapp.com/todos/');
    check(response, {
        "status code should be 200": res => res.status === 200,
    });
}

The code inside the default function VU code runs when the test is invoked either for a single iteration (one VU) or with multiple iterations for purposes of load testing.

The VU code makes the API HTTP requests and carries out assertions, but it cannot import other modules or load files from the file system. That is how k6 knows which tests to keep in memory when executing in different execution modes. This strategy improves performance in k6 and optimizes test design.

Now that this is getting interesting, it is time to integrate CI/CD into our application so you can execute the tests. For this tutorial, we will use CircleCI to set up CI/CD for the application.

Note: If you have already cloned the project repository, you can skip this part of the tutorial. I have added the steps here if you want to learn how to set up your own project.

Setting up Git and pushing to CircleCI

To set up CircleCI, initialize a Git repository in the project by running the following command:

git init

Next, create a .gitignore file in the root directory. Inside the file add node_modules to keep npm-generated modules from being added to your remote repository. Then, add a commit and push your project to GitHub.

Log in to CircleCI and go to the Projects dashboard. You can choose the repository that you want to set up from a list of all the GitHub repositories associated with your GitHub username or your organization. The project for this tutorial is named http-request-testing-with-k6. On the Projects dashboard, select the option to set up the project you want. Choose the option to use an existing configuration.

Note: After initiating the build, expect your pipeline to fail. You still need to add the customized .circleci/config.yml configuration file to GitHub for the project to build properly.

Setting up CircleCI

Create a .circleci directory in your root directory and then add a config.yml file. The config file holds the CircleCI configuration for every project. Execute your k6 tests using the CircleCI k6 orb in this configuration:

# snippet from .circleci/config.yml configuration file 
version: 2.1
orbs:
 k6io: k6io/test@1.1.0
workflows:
 load_test:
   jobs:
     - k6io/test:
         script: todos-testing.js

Using third-party orbs

CircleCI orbs are reusable packages of yaml configurations that condense code into a single line. To allow the use of third-party orbs like python@1.2, you may need to:

  • Enable organization settings if you are the administrator, or
  • Request permission from your organization’s CircleCI admin.

After setting up the configuration, push the configuration to GitHub. CircleCI will start building the project.

Voila! Go to the CircleCI dashboard and expand the build details. Verify that the tests ran successfully and were integrated into CircleCI.

Pipeline Setup Success

Now that you have your CI pipeline set up, you can move on to writing more HTTP request tests with k6.

Verifying k6 responses

Now that you have learned about the structure of k6 and how to write a basic test, the next step is writing scalable tests in k6. Use your todo API to create a todo item with the name write k6 tests:

// snippet from ./todos-testing.js
  group('Create a Todo', () => {
       const response = http.post('https://todo-app-barkend.herokuapp.com/todos/',
       {"task": "write k6 tests"}
       );
       todoID = response.json()._id;
       check(response, {
           "status code should be 200": res => res.status === 200,
       });
       check(response, {
           "Response should have created todo": res => res.json().completed === false,
       });
   })

This code block passes a todo item and makes an HTTP request to our Todo app API, and your todo item is created. Easy, right?

To make this more interesting, add another test to fetch the created todo item. The challenge here is getting access to the scope of the previous test. To do that, create a global scope of the returned response of the previous test. Then use the Id of the todo item in the subsequent test. Here is an example:

// snippet from ./todos-testing.js
group('get a todo item', () => {
    const response = http.get(`https://todo-app-barkend.herokuapp.com/todos/${todoID}`
       );
    check(response, {
        "status code should be 200": res => res.status === 200,
    });
    check(response, {
        "response should have the created todo": res => res.json()[0]._id === todoID,
    });
    check(response, {
        "response should have the correct state": res => res.json()[0].completed === false,
    });
   })

This test verifies that the created todo item is the same one you created and that its properties have not changed. Running this test locally passes successfully, showing that, just like in JavaScript tests, k6 can share state within tests.

Also shown in this sample code, using groups() to separate tests that are not related adds loose coupling in the event of failures in the tests. It also improves the readability of the tests.

Configuring k6 for multiple environments using scenarios

As a load testing tool, k6 comes bundled with awesome features for performing actions like writing tests in different environments without a lot of configuration. The scenario feature is a great example. Scenarios provide for in depth configuration of k6 tests, making it possible to configure every individual VU according to the configuration preferences.

export let options = {
   scenarios: {
     example_scenario: {
       env: { EXAMPLEVAR: 'testing' },
       tags: { example_tag: 'testing' },
     },
   }
 }

This code block shows how the scenarios object is bundled within the k6 options object. It passes the env object that contains our environment configuration for different environments. Gold mine!

To do this yourself, create a file called environmentConfig.js or use the one from the cloned repository. Add a configuration for the development and the staging environment, and pass them to different tests.

// snippet from ./environmentConfig.js
export function developmentConfig(filename) {
   let envConfig = {
     BASE_URL: "http://todo-app-barkend.herokuapp.com/todos/",
     ENV: "DEVELOPMENT",
   };
   return Object.assign({}, envConfig, filename);
 }
  export function stagingConfig(filename) {
   let envConfig = {
     BASE_URL: "https://todo-app-barkend.herokuapp.com/todos/",
     ENV: "STAGING",
   };
   return Object.assign({}, envConfig, filename);
 }

This code block shows how to configure tests for different environments. Note that it uses an HTTP (non-SSL) URL for the development environment and an HTTPS (SSL-enabled) URL for the staging environment.

Once you have set up the variables for individual environments, you can import them into the different tests with the options object using scenarios. Create two new files to test this feature, one for the development environment and one for the staging environment. Use this snippet for the development environment test:

// snippet from scenario-tests/todos-development-tests.js
import { developmentConfig } from '../environmentConfig.js';
 
export const options = {
   scenarios: {
     example_scenario: {
       env: developmentConfig(),
       executor: 'shared-iterations',
       vus: 1
     }
   }
 };
 
export default function () {
   group('API uptime check - development', () => {
       const response = http.get(`${__ENV.BASE_URL}`);
       check(response, {
           "status code should be 200": res => res.status === 200,
       });
   });
…
});

This code block shows how to utilize the scenarios configuration while running the same set of tests in different environments.

Note: The complete tests with different configuration for staging and development can be found in the scenarios-tests folder in the cloned repository.

Using this approach, you can run tests in DEV, UAT, or even PROD environments, even when the configurations of the same tests are different for each environment. To verify success, you can run the tests in the scenario-tests folder.

For DEV environment/configuration, run:

K6 run scenario-tests/todos-development-tests.js

For staging environment, run:

K6 run scenario-tests/todos-staging-tests.js

To make sure that these tests run on the CI/CD pipeline, you will need to modify .circleci/config.yml to include these run commands:

# snippet from .circleci/config.yml configuration file 
    jobs:
      - k6io/test:
          script: todos-testing.js
      # Add more jobs here
      - k6io/test:
          script: scenario-tests/todos-development-tests.js
      - k6io/test:
          script: scenario-tests/todos-development-tests.js 

Scenarios let you configure k6 tests as individual iterations. They provide extensibility when you want to write multiple load test scenarios in just one file, using a different configuration for each scenario.

Verifying pipeline success in CircleCI

Through the tutorial, we have been able to run our tests successfully, but that does not validate that they will always work on CI. To increase our confidence in the process, let’s commit our changes and push to GitHub again. Since we have already configured CircleCI, the build should automatically start once changes have been pushed to the GitHub remote repository.

Pipeline Run Success

Hurray! We got green checks on all our builds for the three test files. That can only mean one thing: celebration time!

Conclusion

In this tutorial, we got acquainted with what the k6 framework is and how it works. We got to learn that it is created in goja language but is written to work with ES6 JavaScript syntax, which optimizes it for performance as a load testing tool. We also learned the different lifecycle stages of a k6 test and also how we can write and assert k6 tests. We finished by learning how to configure k6 in multiple environments and utilizing the options and scenarios objects in k6. Now go forth and prosper!


Waweru Mwaura is a software engineer and a life-long learner who specializes in quality engineering. He is an author at Packt and enjoys reading about engineering, finance, and technology. You can read more about him on his web profile.