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

## Before you start

You need one service in `testkit.config.ts`. [Get started](https://turnstone.cloud/quickstart) adds it in step two.

## Write the test

Create the file in a `__testkit__` directory beside the code it tests.

```ts title=src/checkout/__testkit__/refund.contract.testkit.ts
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

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

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

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

The 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](https://turnstone.cloud/docs/bug-reports.md) 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.

```ts
contractScenario("the export finishes", { timeoutMs: 20_000 }, async ({ http }) => {
	const response = await http().post("/exports");
	expect(response.status).toBe(202);
});
```
