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>
3.5 KiB
3.5 KiB
name, description
| name | description |
|---|---|
| test-strategy | 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, noTestBed.- Value-object parser → happy path + normalisation + each rejection. Assert on the
Resultdiscriminant (.ok) and the parsed value, not the error message. infrastructure/parse*(trust boundary) → required spec. Accept a valid DTO; rejectnull,{}, and malformed shapes. Name itdescribe('… (trust boundary)').application/stores/commands → spec the pure reduce + optimistic begin→confirm/rollback + the commandResult.ui/→ Storybook story, not a component test. Axe runs on every story; add aplayonly for wiring axe can't see.- Never assert on
$localizecopy. It changes per locale/edit — assert on theResult, the value object, or the message id. - Fixtures go through the production door, never a hand-built literal (ADR-0006).
Replay real
Msgs through the realreduce(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-boundaryparse*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:
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
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