Knowledgebase

Migrating from Lua to JS (k6)

Lua and JS share most of the fundamental logical constructs and control flow mechanisms that are commonly found in general purpose programming languages. Same goes for the load testing oriented APIs that we’ve added in each respective product. This section will look at how to convert Lua APIs into the JS equivalent.

High-level differences

On highest level there are some differences to be aware of before we continue on into more details.

Loading of builtin modules and APIs

Lua

In Lua all the available functionality is loaded by default, APIs can be called right away without explicit loading/importing:

1
2
http.get("https://test.loadimpact.com/")
client.sleep(3)

JS

In JS you need to explicitly import the builtin modules and APIs that you want to use:

1
2
3
4
5
6
7
import {sleep} from "k6";
import http from "k6/http";

export default function() {
  http.get("https://test.loadimpact.com/");
  sleep(3);
}

Scope of VU code

Lua

VUs execute the script from top to bottom over and over:

1
// The VU code is the same as global scope, and gets run over and over by a VU

JS

VUs execute the global scope (aka “init code”) once to initialize, and then executes the “main function” (export default function) over and over:

1
2
3
4
5
// Imports and other global scope code

export default function() {
  // The VU code, that gets run over and over by a VU
}

Converting Lua APIs to JS APIs

Client sleep/think time

To have a VU sleep or think for a specific amount of time (in the example below for 3 seconds), pausing the VU execution, you would write Lua code like this:

1
client.sleep(3.0)

the equivalent in JS would be:

1
2
3
4
5
import {sleep} from "k6";

export default function() {
  sleep(3);
}

Making requests

To make HTTP requests there are a number of different Lua APIs available. In the end they’re all wrappers around the http.request_batch() API. Here are the common ways to make requests in Lua code:

1
2
3
4
5
6
7
8
9
10
11
-- Send a single GET request
http.get("https://httpbin.org/")

-- Send a single POST request
http.post("https://httpbin.org", "key=val&key2=val")

-- Send several requests in parallel
http.request_batch({
  { "GET", "https://httpbin.org/" },
  { "POST", "https://httpbin.org/", "key=val&key2=val" }
})

the equivalent in JS would be:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import http from "k6/http";

export default function() {
  // Send a single GET request
  http.get("https://httpbin.org/");

  // Send a single POST request
  http.post("https://httpbin.org", "key=val&key2=val");

  // Send several requests in parallel
  http.batch([
    "https://httpbin.org/",
    { method: "POST", url: "https://httpbin.org/", body: "key=val&key2=val" }
  ]);
}

See the HTTP API docs for k6 for more information and examples.

Group requests and logic into transactions/pages

In the 3.0 product there’s a concept of pages. Lua code in between calls to http.page_start() and http.page_end() will be be measured to provide a page load times in the results:

1
2
3
4
5
6
7
http.page_start("My page")
http.get("https://httpbin.org/")
http.request_batch({
  { "GET", "https://httpbin.org/" },
  { "GET", "https://httpbin.org/get" },
})
http.page_end("My page")

the equivalent in JS would be to use Groups:

1
2
3
4
5
6
7
8
9
10
11
import http from "k6/http";

export default function() {
  group("My page", function() {
    http.get("https://httpbin.org/");
    http.batch([
      "https://httpbin.org/",
      "https://httpbin.org/get",
    ]);
  });
}

Data store

In the 3.0 product there’s a concept of a Datastore. A CSV file that you can upload to the service and then attach to your user scenario for accessing and using the data in your user scenario logic.

In the 4.0 product there’s no specific concept of a Datastore, but in k6 you have two different ways to separate test parameterization data from script logic.

Both of the examples below can be run with:

k6 run --vus 3 --iterations 3 script.js

Use the open() scripting API to open a CSV/JSON/TXT file:

users.json:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[
  {
    "username": "user1",
    "password": "password1"
  },
  {
    "username": "user2",
    "password": "password2"
  },
  {
    "username": "user3",
    "password": "password3"
  }
]

script.js:

1
2
3
4
5
6
7
8
9
import { sleep } from "k6";

const users = JSON.parse(open("./users.json"));

export default function() {
  let user = users[__VU - 1];
  console.log(`${user.username}, ${user.password}`);
  sleep(3);
}

Put the data in a JS file and import it as a module:

userData.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export let users = [
  {
    "username": "user1",
    "password": "password1"
  },
  {
    "username": "user2",
    "password": "password2"
  },
  {
    "username": "user3",
    "password": "password3"
  }
];

script.js:

1
2
3
4
5
6
7
8
import { sleep } from "k6";
import { users } from "./userData.js"

export default function() {
  let user = users[__VU - 1];
  console.log(`${user.username}, ${user.password}`);
  sleep(3);
}

Custom metrics

Beyond the standard metrics collected by the 3.0 product you can also collect custom metrics using the results.custom_metric() API:

1
2
3
-- Track the time-to-first-byte (TTFB)
local res = http.get("https://httpbin.org/")
result.custom_metric("time_to_first_byte", res.time_to_first_byte)

the equivalent in JS would be to use the Trend custom metric:

1
2
3
4
5
6
7
8
9
10
11
12
import {group} from "k6";
import http from "k6/http";
import {Trend} from "k6/metrics";

let ttfbMetric = new Trend("time_to_first_byte");

export default function() {
  group("My page", function() {
    let res = http.get("https://httpbin.org/");
    ttfbMetric.add(res.timings.waiting);
  });
}

For more information, see the custom metrics k6 docs (there Counter, Gauge and Rate metric types beyond the Trend one used above).