# Test files

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

## Put tests beside the code

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

```text title=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

| 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

```ts
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](https://turnstone.cloud/docs/contract-tests.md) |
| `int`, `e2e`, `load` | `k6` | `k6-http` | [Write an HTTP test](https://turnstone.cloud/docs/http-tests.md) |
| `ui` | `playwright` | `playwright-ui` | [Write a browser test](https://turnstone.cloud/docs/browser-tests.md) |
| `dal` | `k6` | `k6-dal` | [Test the database](https://turnstone.cloud/docs/database-tests.md) |
| `data` | `node-vitest` | `node-vitest-data` | [Data tests](#data-tests) |

## See what Testkit found

`testkit discover` lists every test file without running one.

```console $ 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

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

The run prints the file and the reason.

```console $ 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`](https://turnstone.cloud/docs/configuration.md#skip) field. `testkit run --ignore-skip-rules` runs skipped files anyway.

## Keep two files apart with a lock

```ts
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.

```js title=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.

```console $ 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.

```ts
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](https://turnstone.cloud/docs/contract-tests.md).
