Files
ehoandClaude Sonnet 5 a82332fa20 docs: ADR-0006 test-data builders, close out WP-70
Writes up the principle behind WP-70's three tracks ("build test data
through the same door production code uses") as ADR-0006, with a decision
table for which fixture idiom fits which test type. Updates the
test-strategy skill (adds the Fixtures rule, fixes its stale pre-monorepo
src/app/... worked-example paths) and the shared Storybook testing.mdx page
to match. Closes WP-70 with the signatures/counts as actually shipped.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 15:31:20 +02:00

79 lines
3.5 KiB
Markdown

---
name: test-strategy
description: Place tests the house way — Vitest specs co-located by layer (pure domain, no TestBed; parse* trust boundaries; thin UI via Storybook a11y). Use whenever adding a spec or deciding what to test.
---
# Test strategy (test where it's pure)
Push logic down to where it's pure, test it there directly, keep the layers above thin.
No `TestBed` for domain. Never assert on user-facing copy.
## Rules
- **`domain/` + any pure logic → required spec.** Reducers, combinators, `visibleSteps`,
policies, parsers. Import the function and call it — no Angular, no `TestBed`.
- **Value-object parser → happy path + normalisation + each rejection.** Assert on the
`Result` discriminant (`.ok`) and the parsed value, **not** the error message.
- **`infrastructure/` `parse*` (trust boundary) → required spec.** Accept a valid DTO;
reject `null`, `{}`, and malformed shapes. Name it `describe('… (trust boundary)')`.
- **`application/` stores/commands → spec** the pure reduce + optimistic
begin→confirm/rollback + the command `Result`.
- **`ui/` → Storybook story, not a component test.** Axe runs on every story; add a `play`
only for wiring axe can't see.
- **Never assert on `$localize` copy.** It changes per locale/edit — assert on the
`Result`, the value object, or the message id.
- **Fixtures go through the production door, never a hand-built literal** (ADR-0006).
Replay real `Msg`s through the real `reduce` (`given(reduce, initial)(...msgs)`) for a
state machine; `unwrapOk(parseX(raw))` for a value object; a type-state builder
(`Given.Concept().Submitted()...`) for a backend aggregate with an ordered lifecycle.
The one deliberate exception is a trust-boundary `parse*` spec, below — there the fixture
must be a raw, possibly-malformed literal, because the test's whole point is "what if this
shape is wrong." See ADR-0006's decision table for which idiom fits which test type.
## Skeleton
Co-locate `*.spec.ts` next to the unit, in the same layer folder:
```
<context>/domain/<thing>.spec.ts # pure — no TestBed
<context>/domain/value-objects/<vo>.spec.ts # parser: ok + normalise + each reject
<context>/infrastructure/<x>.adapter.spec.ts# parse* trust boundary
<context>/application/<store|command>.spec.ts
```
Minimal parser spec:
```ts
import { describe, it, expect } from 'vitest';
import { parseThing } from './thing';
describe('parseThing', () => {
it('accepts + normalises', () => {
const r = parseThing(' raw ');
expect(r.ok).toBe(true);
if (r.ok) expect(r.value).toBe('RAW');
});
it('rejects malformed', () => {
expect(parseThing('').ok).toBe(false); // asserts the tag, not the copy
});
});
```
## Worked examples
- `apps/ssp/src/app/registratie/domain/value-objects/postcode.spec.ts` — parser style.
- `apps/ssp/src/app/registratie/infrastructure/brp.adapter.spec.ts` — trust boundary (`null`/`{}`).
- `apps/ssp/src/app/registratie/domain/registratie-wizard.machine.spec.ts` — pure reducer.
- `libs/shared/src/testing/{machine,remote-data,value-object}.ts` — the shared fixture
helpers (ADR-0006); `apps/ssp/src/app/herregistratie/domain/intake.testing.ts` — a
per-context wrapper (`givenIntake = given(reduce, initial)`); `intake.acceptance.spec.ts`
— a full journey expressed as one replayed message sequence.
## Verify
```bash
npm test # Vitest (ng test — no vitest.config)
npm run test-storybook # axe over every story (UI a11y gate)
cd backend && dotnet test # backend rule + endpoint + golden tests
```