K6 Development: Beyond The Basic Setup
Tags: k6, loadtesting, tutorial, automation
Published: 2023-09-26 03:40:04

k6 is a pretty great load testing tool. However, like many test tools it seems easy to use based on the [getting started documentation](https://k6.io/docs/get-started/running-k6/) or [most](https://dev.to/viniciussouza/creating-load-tests-with-k6-3lad) [articles](https://www.lambdatest.com/blog/k6-testing-tutorial/) you find on the web, but get tricky pretty fast if you want to have a more complex development environment. 

> I am not knocking these articles, because they are valuable and many people are probably fine with the basic setup. However, it does not work well for distributing to multiple teams or long term scalability.

## Overview

What makes a more complicated development environment? Well, here are some things I ran into while developing a load test suite based on k6:

* Using Typescript
* Using Node modules (external and from a custom API library)
* Sharing k6 utility files

There were many more things that went into making a robust k6 solution, but I will cover them in future articles. This one will focus on setting up your development environment to be able to handle the above list.

A slightly simplified version of what I was building looks like this:

> There is sample code you can refer to at my [playwright-template-complex-dev respository](https://github.com/felicia-walker/playwright-template-complex-dev) which covers all sections of this article. It is a simplified version of the diagram below.

![UML diagram of a simplified code setup](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/354xt8vzcb7x0owz3ny8.png)

The k6 functionality of calling API endpoints was put into its own library. This was done to make sharing with other teams easier. This library in turn depends on external libraries as well as another custom API library. The API library helps build web request urls and payloads for an API test suite. It made sense to leverage this instead of recoding all of those endpoints in k6.

Both this load library and the API library are published to a private NPM repository. The actual k6 script pulls from this, as well as the public repository, for the libraries it needs. It can also import any local files specific to its tests. The use of the load library greatly reduced the size and duplication of code in the k6 scripts.

> I won't cover how to set things up to access a private NPM registry since it depends on a lot of things outside the scope of this article.

As a final note, the load library and scripts each reside in different Git repositories. This was done for NPM package version reasons, but also to keep the checkin histories and processes cleaner. It was also suppose to make it easier for other teams to update the libraries, if needed.

## A Bundler: The Key To Everything

A bundler is software that combines Javascript files and dependencies into a single file. Since the k6 engine is Go based and runs scripts in an embedded way, it expects to only pull in single JS files with no dependencies. Therefore, if you do anything with `import`s or non-vanilla JS, you will need to bundle.

There are many bundlers out there, but for this article we will be using [webpack](https://webpack.js.org/). It is a bit older, but it's what k6 uses in [its examples](https://k6.io/docs/using-k6/modules/) and what I got to work.

## Getting Typescript To Work

Basically, you install Typescript, set up a bundler, and use that to compile the Typescript into a single executable file. This [sample repository](https://github.com/grafana/k6-template-typescript) is pretty spot on for doing just this. 

> The [example repository for this article](https://github.com/felicia-walker/k6-template-complex-dev) also works, but contains more than just the basic Tyepscript setup.

Using the Typescript only repository above, you really just need to copy the following files into your script location:

* tsconfig.json
* webpack.config.js
* .bablerc

And make sure the following `devDependencies` are in the _package.json_ file:

```json
"devDependencies": {
    "@babel/core": "7.13.16",
    "@babel/plugin-proposal-class-properties": "7.13.0",
    "@babel/plugin-proposal-object-rest-spread": "7.13.8",
    "@babel/preset-env": "7.13.15",
    "@babel/preset-typescript": "7.13.0",
    "@types/k6": "^0.43.0",
    "@types/webpack": "5.28.0",
    "babel-loader": "8.2.2",
    "babel-plugin-module-resolver": "^5.0.0",
    "clean-webpack-plugin": "4.0.0-alpha.0",
    "copy-webpack-plugin": "^9.0.1",
    "typescript": "^4.7.4",
    "webpack": "5.76.0",
    "webpack-cli": "4.6.0",
    "webpack-glob-entries": "^1.0.1"
  },
```

Run `yarn install` to install, and you should be set. Now just build with `yarn webpack` and a single output JS file will show up in the _/dist_ directory. You can run k6 with this file.

## Importing Node Modules

Since k6 is not Node.js compatible, it does not know how to resolve node modules. Therefore, you need to use a bundler to pull them in. There is some [documentation on using node modules](https://k6.io/docs/using-k6/modules/#bundling-node-modules), but it is fairly basic. The Typescript section above already has most of what is needed for most libraries out there. 

From the Typescript only example, you need to make one modification to the _webpack.config.js_ file. It has the _/node_modules_ directory as excluded, but I seem to remember having to undo this. Replace these sections of this file with this code, and you should be good to go:

```json
  resolve: {
    extensions: ['.ts', '.js', '.mjs'],
    modules: ['node_modules'],
  },
  module: {
    rules: [
      {
        test: /\.ts$/,
        use: 'babel-loader',
      },
    ],
  },
```

### Your own Node modules

Things get tricky when you start importing your own node modules. In [my example](https://github.com/felicia-walker/k6-template-complex-dev) this is the `load-library`. Originally it was in it's own repository and published as an NPM package to a private registry, but in the example it is just another directory for simplicity. 

I found you had to compile and bundle your package correctly or you would have issues with your IDE not recognizing types, webpack not compiling properly, or k6 blowing up when it tried to run your script. Again, I am working from memory so hopefully I have captured all of the details.

The idea is you need both the [CJS and ESM](https://blog.logrocket.com/commonjs-vs-es-modules-node-js/) versions in the package. Typescript and modern Javascript want the ESM versions for use with 'import', but Node and k6 want the older CJS version for use with 'require'. In addition, I found you had to copy the CJS version into the ESM destination directory or k6 would be unhappy. There is probably a more elegant solution, but I could not figure it out.

### How to do it

You will want to have this in your _package.json_ file:

```json
    "type": "commonjs",
    "main": "dist/cjs/index.js",
    "module": "dist/esm/index.mjs",
    "types": "index.d.ts",
    "typesVersions": {
        "*": {
            "somedir/*": [
                "dist/types/somedir/*.d.ts"
            ],
        }
    },
    "exports": {
        ".": {
            "types": "./dist/types/index.d.ts",
            "import": "./dist/esm/index.js",
            "require": "./dist/cjs/index.js"
        },
        "./somedir/*": {
            "types": "./dist/types/somedir/*.d.ts",
            "import": "./dist/esm/somedir/*.js",
            "require": "./dist/cjs/somedir/*.js"
        },
        "./package.json": "./package.json"
    },
    "files": [
        "dist",
        "tsconfig.json"
    ],
```

The two blocks with "somedir" will need to be repeated for each directory within your package. These additions tell how to find the CJS and ESM versions of your modules files as well as the type files for each.

You also will need an _/index.ts_ file in your source root that exports every file in your modules. For the example above, this could look something like:

```typescript
export * from './somedir/xyzzy'
export * from './somedir/plugh'
export { default as FrobozzService } from './somedir/frobozz.service'
```

I ended up [borrowing and modifying a build script](https://github.com/felicia-walker/k6-template-complex-dev/blob/main/load-library/scripts/bundle.ts) from the [FakerJS](https://fakerjs.dev/) project instead of using webpack. To build you need to make the CJS version, the ESM version, and put a copy of the CJS version in the ESM directory.

Once all of this built, you can publish and import into k6 with no issues.

## Using Non-local k6 JavaScript Libraries

There are a number of [k6 provided utility libraries](https://jslib.k6.io/) written in Javascript, but are not installed with k6 itself. You will need to download a local copy of the file or use a dynamic import via HTTP. 

For a local file, if you don't store the file next to your script you can just use a relative path when you import. However, you may want to make the file available for easier sharing by including in your own library module (especially if you make modifications). This will require the same setup and build process as the **Your own Node modules** section above. You just need to include the k6 JS files in the _index.ts_ and _package.json_ files.

## Conclusion

Getting k6 to work with Typescript, node modules, and using JS files can be tricky. You will likely need to do one or more of these if you build out a k6 based load framework.

Setting up your development environment to use Typescript, Node modules, or multiple files is not too difficult. It mainly requires the use of a bundler configured to pull in the proper files and deal with them. When using you own node modules, however, you must make sure you build them correctly or issues will arise with your IDE, webpack, or k6. You must build similarly if you want share any k6 provided JS utilities or modify them to have any dependencies.