What makes a good test
Test behaviour, not implementation
Test behaviour, not implementation
A test that breaks when you rename a private variable is testing the wrong thing.
Test the failure case
Test the failure case
Every function that can fail needs at least one test per failure mode. The happy path is the
case least likely to regress.
Name tests as sentences
Name tests as sentences
it("returns 403 when caller does not own the resource") — the name is the specification.Mock at the boundary
Mock at the boundary
Mock the database, the HTTP client, the external service. Never mock an internal helper — that
tests the mock.
Assert specifically
Assert specifically
expect(result).toBeDefined() is nearly useless. expect(result.status).toBe(403) is a test.No interdependency
No interdependency
Every
it() must run in isolation. Never rely on state set up by a previous block.No inline Date.now() or Math.random() mocks
No inline Date.now() or Math.random() mocks
Inject them as parameters, or use
vi.useFakeTimers().Accessibility assertions
Every new interactive component includes an axe assertion — see Frontend conventions.Security fixes
Three tests, every time: the attack payload, the401 boundary, the 403 boundary.
See Security conventions.
The regression-test gate
If a PR’s diff touches a service or an API route, the same diff must add or modify at least one test file:[no-test]in the HEAD commit subject or body — for a genuine refactor, config or docs-adjacent change.- A path glob in
scripts/regression-test-allowlist.json, with a reason.
The gate exists because a PR once claimed a test in its commit message that was never committed.
CI proves the code compiles and existing tests pass; nothing else proved a logic change came with
a test.
Structural twins
Where two functions must stay in sync by construction — parallel resolvers, an alias map and its consumer, aGET and a POST that share a guard — pin them with a
same-shape test, so divergence fails CI instead of surviving until someone notices.
The alias lockstep test in src/lib/contracts/variables.test.ts is the reference
pattern, and the OpenAPI artifact paths use the same
idea.
Tests that pin process
Some of the repository’s own automation is specified insrc/lib/pipeline/ with tests
next door — the bot-review summary regex, PR readiness, the path classifier. Bash
implementations mirror those specs and are kept honest by npm run test:scripts.
If you change the automation, change the spec and its test together.