Write tests
Test files
Where a test file lives, what its name means, and the one declaration every file needs.
On this page
Put tests beside the code
Testkit discovers test files in __testkit__ directories. Put each __testkit__ directory beside the code it tests.
src/
├── __testkit__/
│ └── health.contract.testkit.ts
├── checkout/
│ └── __testkit__/
│ ├── health.int.testkit.ts
│ └── refund.contract.testkit.ts
└── db/
└── __testkit__/
└── refunds.dal.testkit.tsKeep 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
| Suffix | Type | Use it for |
|---|---|---|
*.contract.testkit.ts | contract | A service boundary that needs Node APIs, fixtures, or file IO |
*.int.testkit.ts | int | A route, service, queue, or job at its runtime boundary |
*.e2e.testkit.ts | e2e | A flow that spans services without a browser |
*.ui.testkit.ts | ui | A page or interaction where the browser is part of the assertion |
*.dal.testkit.ts | dal | Data access and persistence |
*.data.testkit.ts | data | The shape, cost, volume, or latency of data in a real flow |
*.load.testkit.ts | load | Load, 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 type | Engine | Runner profile | Guide |
|---|---|---|---|
contract | node-vitest | node-vitest-contract | Write a contract test |
int, e2e, load | k6 | k6-http | Write an HTTP test |
ui | playwright | playwright-ui | Write a browser test |
dal | k6 | k6-dal | Test the database |
data | node-vitest | node-vitest-data | Data tests |
See what Testkit found
testkit discover lists every test file without running one.
$npx testkit discoverSummary 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)
└── RefundsAdd --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 contractSKIP 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-822d9269In 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.
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 keySetup 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.