Files
ehoandClaude Opus 5 531817259e refactor(shared): delete unwrapOk, the unadopted test value-object helper (RB-33)
unwrapOk had zero consumers in apps/ or libs/ since ADR-0006 shipped it.
The one call site the finding named already satisfies the ADR's real
rule (call the real parser, never a cast) with an inline guard, so
adding a manufactured first caller was not the better fix. This commit
deletes the helper and its file, and updates the one doc sentence that
named it. The finding's call site is unchanged. See rb-33.md for the
full adopt-or-delete reasoning.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 13:14:21 +02:00

125 lines
5.8 KiB
Plaintext

import { Meta } from '@storybook/addon-docs/blocks';
<Meta title="Foundations/Testing strategy" />
# Testing strategy
Tests follow the same grain as the architecture: **push the logic down to where it's pure,
test it there directly, and keep the layers above thin.** No single tool covers everything,
so each layer gets the cheapest test that catches its class of bug. This page owns _what to
test, by layer_; how each test is **phrased and scoped** — one behaviour, in the domain's
language — is [BDD](?path=/docs/foundations-bdd--docs).
## What gets tested where
| Layer | Test kind | Tool | Rule |
| -------------------------- | ------------------------ | ---------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `domain/` | pure-function unit spec | Vitest | **Required.** Call the function directly — **no `TestBed`**. Reducers, combinators, `visibleSteps`, parsers, policies. |
| `domain/value-objects/` | parser spec | Vitest | Happy path + normalisation + **each** rejection. Assert on the `Result`, never the message. |
| `infrastructure/` `parse*` | trust-boundary spec | Vitest | Accept a valid DTO; **reject `null` / `{}` / malformed**. Name it `describe('… (trust boundary)')`. |
| `application/` | store / command spec | Vitest | Reducer purity, optimistic begin→confirm/rollback, command `Result`. |
| `ui/` | Storybook story | Storybook + a11y | Kept thin. Axe runs on every story; add a `play` only for wiring axe can't see. |
| flows | e2e smoke | Playwright | One happy path + one error state per critical journey. |
| backend | rule + endpoint + golden | xUnit | Mirror of the FE domain rules, plus `WebApplicationFactory` integration. |
## Tooling
Vitest runs through Angular's built-in `@angular/build:unit-test` builder — **there is no
`vitest.config.ts`**; config is implicit via `tsconfig.spec.json`.
```bash
npm test # ng test → Vitest, all *.spec.ts co-located next to their unit
```
Specs import `{ describe, it, expect }` from `vitest` and are co-located with the unit
they cover.
## The house style
Pure and direct. A value-object parser spec
(`registratie/domain/value-objects/postcode.spec.ts`):
```ts
import { describe, it, expect } from 'vitest';
import { parsePostcode } from './postcode';
describe('parsePostcode', () => {
it('normalises to "1234 AB"', () => {
const r = parsePostcode(' 1234ab ');
expect(r.ok).toBe(true);
if (r.ok) expect(r.value).toBe('1234 AB');
});
it('rejects malformed input', () => {
expect(parsePostcode('0234AB').ok).toBe(false); // asserts the tag, not the copy
});
});
```
A trust-boundary adapter (`registratie/infrastructure/brp.adapter.spec.ts`) additionally
proves the untrusted shape is rejected:
```ts
expect(parseBrpAddress(null).ok).toBe(false);
expect(parseBrpAddress({}).ok).toBe(false); // missing required field
```
Elm-style machines test the pure `reduce` — no Angular
(`registratie/domain/registratie-wizard.machine.spec.ts`).
## Fixtures: build test data through the production door
A fixture is not a shortcut around the domain — it's the domain's own construction path, run
once for the test. [ADR-0006](../../../docs/reference/architecture/0006-test-data-builders.md)
covers this in full (with a backend example too); the frontend idiom is one combinator,
`given` (`libs/shared/src/testing/machine.ts`):
```ts
export const given =
<S, M>(reduce: (s: S, m: M) => S, initial: S) =>
(...msgs: M[]): S =>
msgs.reduce(reduce, initial);
export const givenIntake = given(reduce, initial); // per-context wrapper, pure TS
```
A machine spec replays real `Msg`s instead of hand-writing a `State` literal — so a fixture
can only ever be a state the real reducer actually produces:
```ts
const atStep3 = givenIntake(Start(), SetUren('1200'), Next(), SetDiplomaHerkomst('NL'), Next());
```
The same rule extends to value objects — call the real `parse*` and check `.ok` before use,
never a cast — and to `RemoteData` (`loading()` / `success(v)` / `failure(e)` in
`libs/shared/src/testing/remote-data.ts` instead of a redefined-per-file literal). **Never**
a `.withX().withY()` builder over an open constructor — that just re-opens whatever illegal
state the domain closed.
## UI = Storybook, not heavy component tests
`@storybook/addon-a11y` runs the `wcag2a/2aa/21a/21aa` rule sets on **every** story;
`@storybook/test-runner` + `axe-playwright` turn that into a CI gate:
```bash
npm run test-storybook # axe over every story against a running Storybook
npm run test-storybook:ci # builds storybook-static, serves :6006, runs the gate
```
Disabling a11y on a story needs an inline justification + a WP cross-reference (see
[Accessibility](?path=/docs/foundations-accessibility--docs)).
## Don't assert on copy
Localized strings change per locale and per edit. Tests assert on the `Result`
discriminant, the value object, or the message **id** — never the rendered Dutch/English
text. Full detail in [Internationalization](?path=/docs/foundations-internationalization--docs).
## The GREEN gate
```bash
npm run lint && npm run check:tokens && npm test && npm run build && npm run build-storybook
cd backend && dotnet test
```
Everything above must pass before a work package is done.