Skip to content

Testing

Minitest with fixtures. No RSpec, no FactoryBot.

bash
mise exec -- bin/rails test              # everything
mise exec -- bin/rails test test/models  # a directory
mise exec -- bin/ci                      # style, security, tests

Layout

DirectoryCovers
test/models, test/libDomain logic
test/controllersRequests, authorisation, rendering
test/policiesAuthorisation predicates
test/helpersPresentation mapping
test/invariantsProduct rules that must hold everywhere
test/design_auditReal-browser screenshots and Turbo interaction, skipped unless DESIGN_AUDIT=1

Do not write down what the code does

Write the assertion you would have written before reading the implementation. A test that records observed output is not a test — it is a lock, and if the code is wrong it locks the wrongness in.

This has happened five times in this codebase:

AssertedShould have been
Both screening gates clear when no screening had runnot_evaluable — nobody looked
match_count: 0 for a PENDING_REVIEW bodynil — the provider had not answered
403 for another account's record404 — 403-vs-404 enumerates tenants
medium_max == scale_max is validRejected — it makes the high band unreachable
A risk_level with no risk_level_updated_at is publishedWithheld — the column defaults to low

They share a mechanism: the case had no obviously right answer, so the value the code produced got written down as the expectation. Code and test then agreed — which is exactly why mutating a single file never caught them. The test was wrong in the same direction.

The rule for this domain: whenever an assertion says a check passed, a count was zero, a score banded or a record was clean, ask what was actually read to justify it. If the answer is "nothing", the expected value is an absence — nil, not_evaluable, "not assessed" — never a reassuring one.

Invariants

test/invariants/ states product rules as executable assertions that walk the real registries — every gate, every public-result adapter, every provenance value — so a component added tomorrow is covered without anyone remembering.

Currently held. never_claim_clear_test.rb carries the core family — the product principle applied layer by layer:

  1. No gate reports a clean outcome from evidence nobody read, and neither does the person readout
  2. No provider adapter reports a positive result from a payload that says nothing
  3. A screening that has not answered has no match count
  4. A nil score never bands, under any configuration
  5. No provenance lets a below-neutral score through from an unevidenced source
  6. No valid tier configuration makes a band unreachable
  7. A date of birth is never held or sent in the wrong calendar

Each remaining file states one principle:

PrincipleFile
A date in the wrong calendar is not a date of birth, on any surface reporting onea_date_in_the_wrong_calendar_is_not_a_date_test.rb
The API never claims a verification that never happenedapi_never_claims_clear_test.rb
A country scope that is advertised is a country scope that is enforcedcountry_scope_is_enforced_test.rb
A check nobody can configure is a check nobody runsevery_check_reaches_the_country_defaults_test.rb
Every permission is filed and named, in every languagepermission_catalog_covers_every_permission_test.rb
A setting the code supplies is never a setting an operator has torequired_config_is_obtainable_test.rb
The role catalogue is a compliance artefact, and no role is both maker and checkerrole_catalogue_test.rb
Screening never searches nothing and calls it cleanscreening_never_searches_nothing_test.rb
A value an operator can set is a value the request carriessettings_reach_the_request_test.rb

See test/invariants/README.md, which is the index these are kept against.

The first run of that suite found a live bug nobody had reported: low_max could be set below the bottom of the scale, leaving the low band unreachable.

When you fix a bug whose test agreed with it, add an invariant rather than only correcting the case.

Mutation-check anything load-bearing

Break the implementation deliberately and confirm the test fails. A green invariant proves nothing until you have seen it go red — one gate invariant here looked correct and was empty, because its fixture tripped an earlier guard and never reached the code under test.

Fixtures

Descriptive names, never one / two. Watch for fixtures that encode a bug: four of the five cases above were fixture-level, including one that reused Elm's returned birth date as the subject's stored claim, conflating what we send with what comes back.

Design audit

bash
DESIGN_AUDIT=1 mise exec -- bin/rails test test/design_audit/

Boots headless Chromium against a disposable seeded database and writes full-page screenshots at 1280px and 390px into tmp/design_audit/. Useful for reviewing a visual change across every screen at once.

Known flake

This suite fails intermittently — roughly one run in three at times — and the cause is not yet identified. If you see a failure here, re-run before investigating the change you just made.