--- 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: ``` /domain/.spec.ts # pure — no TestBed /domain/value-objects/.spec.ts # parser: ok + normalise + each reject /infrastructure/.adapter.spec.ts# parse* trust boundary /application/.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 ```