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>
125 lines
5.8 KiB
Plaintext
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.
|