Write tests
Write a contract test
A contract test runs in Node under Vitest. Testkit starts your service, gives the test an HTTP client that points at it, and stops the service when the run ends.
On this page
Before you start
You need one service in testkit.config.ts. Get started adds it in step two.
Write the test
Create the file in a __testkit__ directory beside the code it tests.
import { defineFile } from "@gfdelarue/testkit";
import { contractScenario, expect } from "@gfdelarue/testkit/node-vitest/contract";
export const testkit = defineFile({ engine: "node-vitest" });
contractScenario("a refund pays out once", async ({ http }) => {
const first = await http().post("/orders/8841/refunds");
const second = await http().post("/orders/8841/refunds");
expect(first.status).toBe(201);
expect(second.status).toBe(409);
});contractScenario registers one case. Name the case after the behaviour you expect, so the name is true when the product is right.
Run it
$npx testkit run contract[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: PASSED
Passed: 2
Failed: 0
Skipped: 0
Not run: 0
Files: 2
Duration: 8s
Run ID: 1791890142106-1343177-e5c04590To run one file, pass --file src/checkout/__testkit__/refund.contract.testkit.ts.
Read a failure
When the service pays the refund twice, the second assertion fails. The run prints the assertion and the line that made it, then the rest of the stack.
$npx testkit run contract[1/2] FAIL api contract src/checkout/__testkit__/refund.contract.testkit.ts 1s
AssertionError: expected 201 to be 409 // Object.is equality at
/repo/src/checkout/__testkit__/refund.contract.testkit.ts:10:24
[2/2] PASS api contract src/__testkit__/health.contract.testkit.ts 1s
Summary
Result: FAILED
Passed: 1
Failed: 1
Skipped: 0
Not run: 0
Files: 2
Duration: 10s
Run ID: 1791890523760-1353387-5113dd97The command exits 1. Leave the test failing until the product is fixed. A test that fails for a real bug is the evidence a bug report needs.
What a scenario receives
| Field | What it is |
|---|---|
http(options?) | Returns an HTTP client for the service. options takes baseUrl and defaultHeaders. |
baseUrl | The URL of the running service |
env | The service's declared environment values, read-only |
fixture(name) | A fixture file, with text(), json(), buffer(), sha256, and sizeBytes |
The client has get, post, put, patch, delete, and request(method, path, body?, headers?). get takes a path and optional headers. The others take a path, an optional body, and optional headers.
Every response has the same fields.
| Field | Type |
|---|---|
status | number |
headers | Record<string, string> |
body | string |
json() | Parses body |
method, path, url | string |
Use Vitest where you need it
expect is Vitest's expect. A contract file is a native Vitest suite, so test.skip(...) and test.todo(...) from vitest work beside contractScenario.
To give one scenario its own time limit, pass options as the second argument.
contractScenario("the export finishes", { timeoutMs: 20_000 }, async ({ http }) => {
const response = await http().post("/exports");
expect(response.status).toBe(202);
});