import { Meta } from '@storybook/addon-docs/blocks'; # Behaviour-driven tests Tests here read as **specifications of behaviour**, not checks of implementation. A test says what the system _does_ — in the domain's own words — so a failing test names a broken behaviour, and the suite doubles as living documentation. This is the BDD half of the [Testing strategy](?path=/docs/foundations-testing-strategy--docs) (which owns _what to test, by layer_); BDD owns _how each test is phrased and scoped_. ## Three rules ### 1. `describe` = the subject, `it` = one observable behaviour, structured Given → When → Then The `describe()` block names the unit under test; each `it()` states a single behaviour in **declarative present tense** — the implicit subject is "it" (no `should`). Present-tense naming and Given/When/Then structure are not in tension — the _title_ stays a declarative one-liner; the _body_ is what's organised as Given → When → Then: ```ts describe('parsePostcode', () => { it('normalises to "1234 AB" (uppercase, single space, trimmed)', () => { // Given a postcode with mixed case, extra whitespace, and no gap before the letters. // When it is parsed... const result = parsePostcode(' 1234ab '); // Then it comes back normalised. expect(result).toEqual(ok('1234 AB')); }); it('rejects malformed input', () => { // (no Given — the input itself IS the setup) When a non-postcode string is parsed... // Then it is rejected. expect(parsePostcode('nope').ok).toBe(false); }); }); ``` Read top-to-bottom it _is_ the spec: "parsePostcode — normalises to 1234 AB; rejects malformed input." **A genuinely empty phase is omitted, not faked with an empty comment.** The rejection test above has no Given worth writing — the malformed literal passed to `parsePostcode` already is the setup — so it degenerates straight to When/Then. Never write `// Given (nothing)` to keep three comments lined up; an omitted phase is the correct, honest shape for a test that doesn't need it. The three phases stay in order (Given before When before Then) whichever of them are present. **This reverses this doc's earlier advice** ("No … Given/When/Then ceremony") — the team decided explicit G/W/T structure earns its keep as the default for every test, not just acceptance tests. What doesn't change: no `should`, present-tense titles, one behaviour per test, ubiquitous-language naming (rules 2–3 below). **The Elm-machine naming style is a sanctioned form of rule-1 naming, not an exception to it.** A store/reducer spec titled after the `Msg` tag it drives — `it('BriefLoaded moves loading to loaded', …)` — names the domain event the same way the reducer's own `switch (msg.tag)` does; the tag IS ubiquitous language for a state machine, so this reads as a present-tense behaviour statement exactly like `'rejects malformed input'` does, not as a violation of rule 3. ### 2. One behaviour per test A test asserts **one behaviour**, not one `expect()`. Several assertions that pin down the _same_ behaviour belong together; assertions about _different_ behaviours belong apart. | Keep together (one behaviour) | Split apart (separate behaviours) | | -------------------------------------------------------------- | -------------------------------------------------------- | | A `Result`'s `.ok` then its `.value` | The `ok` branch **and** the `err` branch of a transition | | A whole-object `toEqual` | An invalid-input case **and** a valid-input case | | A loop asserting one rule over many inputs | Two independent state transitions | | A truth-table (`draft` → true, `approver` → false) of one rule | An authorization check **and** a rendering check | A title that needs `/`, `;`, "then" or "and" to join two behaviours is the smell — split it, and each half gets its own present-tense name. ### 3. Speak the ubiquitous language (the DDD tie-in) Test names use the **domain vocabulary**, not technical jargon — the same words as the [bounded contexts](?path=/docs/foundations-domain-driven-design--docs): a _behandelaar_ drafts, a _beoordelaar_ approves, a _herregistratie_ is _ingediend_. The test name is readable by someone who knows the domain but not the code. ```ts it('drafter cannot approve or reject even when submitted', …); it('confirmed dutch proficiency requires taalvaardigheid proof', …); ``` ## How it fits TDD & DDD - **TDD** — the loop is red → green → refactor: write the behaviour as a failing `it`, make it pass, then clean up. Because tests describe behaviour (not internals), a refactor that preserves behaviour keeps them green. Pure domain logic is tested directly — no `TestBed` (see [Testing strategy](?path=/docs/foundations-testing-strategy--docs)). - **DDD** — behaviour is expressed in the ubiquitous language, so the spec and the code share one vocabulary. Domain rules (reducers, value-object parsers, policies) are the richest specs; the wire boundary is tested as "rejects malformed input", the UI as Storybook stories. ## C#/xUnit shape The three rules above are language-agnostic; xUnit follows them with its own idiom rather than Vitest's `describe`/`it` nesting: - **The method name is the title, in `PascalCase_snake_sentence`** — the same present-tense, ubiquitous-language behaviour statement as a `describe`+`it`, folded into one identifier because xUnit has no nested-description syntax: `Only_open_statuses_are_decidable`, `Afwijzen_requires_a_toelichting`, `A_terminal_besluit_is_frozen`. - **`// Given` / `// When` / `// Then` comments mark the three phases inside the test body** — the same structure as rule 1, made explicit because C# has no BDD framework layered on xUnit here (see ADR-0006 — the language's own test framework plus the builder is enough, deliberately not a Gherkin runner). As in TypeScript, an empty phase is omitted rather than commented for its own sake. - **Fixtures go through the `Given` type-state builder** (ADR-0006 §1), never a field-by-field object initializer — keeping the Given phase itself honest about which states are reachable. ```csharp [Fact] public void A_terminal_besluit_is_frozen() { // Given a case already decided Goedgekeurd — terminal, per BeoordelingRules.CanDecide. var aanvraag = Given.Concept(type: "registratie").Submitted().Decided(Besluit.Goedkeuren).Build(); Persist(aanvraag); // When a behandelaar tries to record a further besluit on it... var (outcome, updated) = ApplicationStore.RecordBesluit(aanvraag.Id, Besluit.Afwijzen, "te laat", DateTimeOffset.UtcNow); // Then the write is refused, and the original decision still stands. Assert.Equal(ApplicationStore.RecordBesluitOutcome.Conflict, outcome); Assert.Null(updated); } ``` See `Acceptance/BesluitLifecycleTests.cs` for the canonical shape (it already does this) and `AuthzTests.cs` for the truth-table naming convention this predates — a `[Theory]` row set stays one behaviour (rule 2's "loop asserting one rule over many inputs"), so it doesn't need per-row G/W/T comments, just one clear method name. ## Where to look Canonical behaviour specs in the repo: `registratie/domain/value-objects/postcode.spec.ts` (parser behaviour), `registratie/domain/registratie-wizard.machine.spec.ts` (the message-driven `describe` block — one reducer transition per test), and backend `Acceptance/BesluitLifecycleTests.cs` (G/W/T-commented behaviour tests) and `AuthzTests.cs` (rule truth-tables). The [Testing strategy](?path=/docs/foundations-testing-strategy--docs) page maps which layer gets which kind of test.