Write tests
Write an HTTP test
An HTTP test drives your running service with k6. Use it for int, e2e, and load files.
On this page
Write the test
import { defineFile } from "@gfdelarue/testkit";
import { defineHttpSuite } from "@gfdelarue/testkit/k6";
import { check } from "k6";
export const testkit = defineFile({ engine: "k6" });
const suite = defineHttpSuite(({ rawReq }) => {
const response = rawReq.get("/health");
check(response, {
"health returns 200": (r) => r.status === 200,
});
});
export const options = suite.options;
export const setup = suite.setup;
export default suite.exec;The last three lines are required. A k6 module exports options, setup, and a default function, and Testkit does not write them for you.
rawReq sends a request with no session. The path is relative to the service's base URL.
Run it
$npx testkit run int[1/2] PASS api int src/account/__testkit__/profile.int.testkit.ts 0s
[2/2] PASS api int src/checkout/__testkit__/health.int.testkit.ts 0s
Summary
Result: PASSED
Passed: 2
Failed: 0
Skipped: 0
Not run: 0
Files: 2
Duration: 5s
Run ID: 1791890151637-1344059-9ba3838fName the check that failed
A failed k6 check fails the file, but the run prints only the threshold.
$npx testkit run int[1/2] PASS api int src/account/__testkit__/profile.int.testkit.ts 0s
[2/2] FAIL api int src/checkout/__testkit__/health.int.testkit.ts 0s
k6 thresholds failed: checks(rate==1.0)Testkit's expect.value takes a value, a predicate, and a label. When the predicate returns false, the run prints the label.
import { defineHttpSuite, expect } from "@gfdelarue/testkit/k6";
const suite = defineHttpSuite(({ rawReq }) => {
const response = rawReq.get("/health");
expect.value(response.status, (status) => status === 200, "health returns 200");
});$npx testkit run int[1/2] PASS api int src/account/__testkit__/profile.int.testkit.ts 0s
[2/2] FAIL api int src/checkout/__testkit__/health.int.testkit.ts 0s
health returns 200
k6 thresholds failed: checks(rate==1.0)Sign in as named actors
Put the sign-in code in a helper beside the tests. An auth adapter has two functions. setup signs every actor in once and returns their sessions. headers turns a session into request headers.
import type { AuthAdapter, AuthSessionBundle } from "@gfdelarue/testkit/k6";
export function sessionAuth(actorNames: string[]): AuthAdapter<AuthSessionBundle> {
return {
setup({ env, http }) {
const actors = Object.fromEntries(
actorNames.map((actorName, actorIndex) => {
const login = http.post(`${env.BASE}/login`, JSON.stringify({ name: actorName }));
const token = String(JSON.parse(String(login.body)).token);
return [actorName, { actorIndex, actorName, session: { token } }];
}),
);
return { actors, primaryActor: actorNames[0] };
},
headers(bundle, { actor }) {
const session = bundle?.actors[actor ?? bundle.primaryActor ?? ""]?.session;
return session ? { Authorization: `Bearer ${session.token}` } : {};
},
};
}Pass the adapter to defineHttpSuite. req sends requests as the primary actor, and req.as(name) sends them as another one.
import { defineFile } from "@gfdelarue/testkit";
import { defineHttpSuite } from "@gfdelarue/testkit/k6";
import { check } from "k6";
import { sessionAuth } from "./helpers/session.js";
export const testkit = defineFile({ engine: "k6" });
const suite = defineHttpSuite({ auth: sessionAuth(["reviewer", "outsider"]) }, ({ rawReq, req }) => {
check(req.get("/me"), {
"the reviewer reads their profile": (r) => r.status === 200,
});
check(req.as("outsider").get("/me"), {
"an outsider is refused": (r) => r.status === 403,
});
check(rawReq.get("/me"), {
"a request with no session is refused": (r) => r.status === 401,
});
});
export const options = suite.options;
export const setup = suite.setup;
export default suite.exec;Three checks cover the signed-in user, the wrong user, and no user. Most access bugs sit in the second one.
Wait for work to finish
waitFor calls a function until a predicate accepts its result. It spends the file's time budget, so do not set a timeout of your own.
import { parse, waitFor } from "@gfdelarue/testkit/k6";
const response = waitFor(
() => req.get("/api/v1/jobs/123"),
(res) => parse.json(res).data?.status === "completed",
{ description: "job 123 to complete" },
);Set the budget for every file with execution.fileTimeoutSeconds, or for one run with --file-timeout-seconds.
Use k6 directly
Native k6 imports work in every HTTP test.
import { check, group } from "k6";
import http from "k6/http";
import { Rate, Trend } from "k6/metrics";To set k6 options such as vus and iterations, name an HTTP profile in profiles.http and pass { profile: "name" } to defineHttpSuite. A load file uses the same suite shape as an int file.