Docs

Write tests

Test files

Where a test file lives, what its name means, and the one declaration every file needs.

View as Markdown

On this page
  1. Put tests beside the code
  2. The filename picks the test type
  3. Every file declares its engine
  4. See what Testkit found
  5. Skip a file with a reason
  6. Keep two files apart with a lock
  7. Run Node code before a file
  8. Data tests

Put tests beside the code

Testkit discovers test files in __testkit__ directories. Put each __testkit__ directory beside the code it tests.

A service with four suites
src/
├── __testkit__/
│   └── health.contract.testkit.ts
├── checkout/
│   └── __testkit__/
│       ├── health.int.testkit.ts
│       └── refund.contract.testkit.ts
└── db/
    └── __testkit__/
        └── refunds.dal.testkit.ts

Keep each __testkit__ directory flat. When a narrower piece of code needs its own tests, add another __testkit__ directory beside that code. Shared helpers can live in __testkit__/helpers/.

The suite name comes from the directory that holds __testkit__, and the test type goes in front of it. In the tree above, refund.contract.testkit.ts belongs to the suite contract:checkout, and refunds.dal.testkit.ts belongs to dal:db.

The filename picks the test type

SuffixTypeUse it for
*.contract.testkit.tscontractA service boundary that needs Node APIs, fixtures, or file IO
*.int.testkit.tsintA route, service, queue, or job at its runtime boundary
*.e2e.testkit.tse2eA flow that spans services without a browser
*.ui.testkit.tsuiA page or interaction where the browser is part of the assertion
*.dal.testkit.tsdalData access and persistence
*.data.testkit.tsdataThe shape, cost, volume, or latency of data in a real flow
*.load.testkit.tsloadLoad, soak, or pressure

Every file declares its engine

import { defineFile } from "@gfdelarue/testkit";

export const testkit = defineFile({ engine: "node-vitest" });

Import defineFile from the root package and pass it an object literal. Testkit never infers the engine from the filename or from the imports. Discovery fails on a missing declaration, on a computed engine value, and on a pair that this table does not list.

Test typeEngineRunner profileGuide
contractnode-vitestnode-vitest-contractWrite a contract test
int, e2e, loadk6k6-httpWrite an HTTP test
uiplaywrightplaywright-uiWrite a browser test
dalk6k6-dalTest the database
datanode-vitestnode-vitest-dataData tests

See what Testkit found

testkit discover lists every test file without running one.

$npx testkit discover
Summary 6 files · 6 active · 0 skipped · 6 suites · 1 services

api (6 files)
├── UI UI
│   └── Web (1 files · playwright · playwright-ui)
│       └── Checkout
├── INT Integration
│   ├── Account (1 files · k6 · k6-http)
│   │   └── Profile
│   └── Checkout (1 files · k6 · k6-http)
│       └── Health
├── CONTRACT Contract
│   ├── Checkout (1 files · node-vitest · node-vitest-contract)
│   │   └── Refund
│   └── Src (1 files · node-vitest · node-vitest-contract)
│       └── Health
└── DAL DAL
    └── Db (1 files · k6 · k6-dal)
        └── Refunds

Add --output-mode verbose for paths and file ids, or --json for a document a script can read.

Skip a file with a reason

export const testkit = defineFile({
	engine: "node-vitest",
	skip: "The payout sandbox is down until Friday",
});

The run prints the file and the reason.

$npx testkit run contract
SKIP api contract src/checkout/__testkit__/payout.contract.testkit.ts 0s The payout sandbox is down until Friday
[1/2] PASS api contract src/checkout/__testkit__/refund.contract.testkit.ts 1s
[2/2] PASS api contract src/__testkit__/health.contract.testkit.ts 1s

Summary
Result: FAILED
Passed: 2
Failed: 0
Skipped: 1
Not run: 0
Files: 3
Duration: 10s
Run ID: 1791890889522-1447900-822d9269

In Testkit 0.4.1, a run that skips a file exits 1, even when every other file passes. To get a passing run, select the other files with --file, --suite, or a type.

To skip a whole suite, or to keep skip rules in one place, use the service's skip field. testkit run --ignore-skip-rules runs skipped files anyway.

Keep two files apart with a lock

export const testkit = defineFile({
	engine: "k6",
	locks: ["global-worker-loop"],
});

Files that name the same lock do not run at the same time. Use a lock when two files change the same shared state and --task-concurrency is above 1.

Run Node code before a file

A test file can have a sibling setup module with the same name and a .setup.mjs ending. Testkit runs its setup export before it starts the engine.

src/checkout/__testkit__/refund.contract.testkit.setup.mjs
export async function setup({ sidecars }) {
	const capture = await sidecars.httpCapture({ path: "/webhook" });
	return {
		data: { webhookUrl: capture.url, requestsUrl: capture.requestsUrl },
	};
}

The setup function receives deadlineMs, signal, env, leaseDir, productDir, serviceName, testFile, and sidecars. Return plain JSON, or { data, cleanup }. Testkit passes data to the engine as setupData and calls cleanup when the file finishes. data can hold at most 1 MiB of JSON.

If setup throws, the file fails and the run names the setup module.

$npx testkit run --file src/checkout/__testkit__/refund.contract.testkit.ts
[1/1] FAIL api contract src/checkout/__testkit__/refund.contract.testkit.ts 0s
    Runtime node setup "src/checkout/__testkit__/refund.contract.testkit.setup.mjs" failed: The
    payment sandbox refused the key

Setup runs in a Node worker thread with the permissions of the host process. It is not a sandbox.

Data tests

A data test measures data moving through a real flow: its shape, volume, cost, and latency. It runs in Node under Vitest.

import { defineFile } from "@gfdelarue/testkit";
import { dataScenario } from "@gfdelarue/testkit/node-vitest/data";
import { expect } from "vitest";

export const testkit = defineFile({ engine: "node-vitest" });

Register every case with dataScenario(...). The data runner rejects a case registered any other way, and testkit lint reports the import. A data test must record evidence through the context that dataScenario provides. A lone count(...) or event(...) call is not enough. For plain Node assertions, write a contract test.

  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.