Docs

Write tests

Write an HTTP test

An HTTP test drives your running service with k6. Use it for int, e2e, and load files.

View as Markdown

On this page
  1. Write the test
  2. Run it
  3. Name the check that failed
  4. Sign in as named actors
  5. Wait for work to finish
  6. Use k6 directly

Write the test

src/checkout/__testkit__/health.int.testkit.ts
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-9ba3838f

Name 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.

src/account/__testkit__/helpers/session.ts
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.

src/account/__testkit__/profile.int.testkit.ts
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.

  1. OverviewStartWhat Testkit is, and what you need to run it.
  2. Get startedStartFive steps from install to a merge gate.
  3. Test filesWrite testsWhere a test lives and what its name means.
  4. Contract testsWrite testsCall your running service from Node.
  5. HTTP testsWrite testsDrive routes with k6, signed in as named actors.
  6. Browser testsWrite testsAssert on a real page in Chromium.
  7. Database testsWrite testsRun SQL against a Postgres that Testkit builds.
  8. CIRunOne command decides whether a pull request merges.
  9. ResultsRunWhere a run's evidence lands and how to read it.
  10. CLIRunEvery command and flag.
  11. ConfigurationRunEvery field of testkit.config.ts.
  12. CampaignsHunt bugsGive your agent a target and collect proven bugs.
  13. Bug reportsHunt bugsThe five things every issue states.
  14. TurnstoneTurnstoneWhat the dashboard keeps, and what it never stores.
  15. Connect GitHubTurnstoneConnect a repository, and see the access Turnstone asks for.
  16. Connect GiteaTurnstoneAdd your Gitea host, then connect a repository.
  17. SyncTurnstoneWhen Turnstone reads your issues, and what it reads.
  18. FactsTurnstoneEach fact about a bug, and where Turnstone reads it.