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
A retry replaces what it retried — a fixed failure stops blocking, a new one startsa_superseded_run_is_not_a_current_one_test.rb
A check offered is a check that can run in that countrya_check_offered_is_a_check_that_can_run_test.rb
A contract is bound to the document that was showna_contract_is_bound_to_the_document_shown_test.rb
An unknown price is never zeroan_unknown_price_is_never_zero_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
Signing is never offered where it cannot complete, and unresearched rules are never reported as nonesigning_is_never_offered_where_it_cannot_complete_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.

A vacuous invariant is worse than none

Three of these have shipped in a state where they asserted nothing — a loop over a collection that was empty, and a guard that early-returned when it found a file on disk. Each passed while proving nothing.

Rails reports "Test is missing assertions" for the loop case; the early-return case reported nothing at all. When writing one, check that it goes red for the right reason and that it executes assertions at all.

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.

Tests that pass for the wrong reason ​

Two, both found on the same branch, both invisible on a developer machine.

Never remove a method you did not define ​

A test stubbed Entity#create_periodic_review! by defining an override, then called remove_method in its ensure to clean up. The production method is defined directly on Entity rather than in a module, so removing the override removed the only definition — for the rest of the process.

Every later test in the same worker that reached SchedulePeriodicReviewsJob then failed with "undefined method create_periodic_review! for an instance of Entity", and which ones failed depended on the random seed. A passing order exists, which is why nobody had seen it.

Stub through a subclass or a prepended module, or restore the original UnboundMethod — never delete a method the test did not create.

A connection built by hand must carry the whole config ​

A test opened its own Postgres connection with the host, port and user from the database config, and not the password. Local Postgres trusts local users, so it passed on every machine; CI's asks for a password, and it failed there with "no password supplied".

If a test must connect directly, pass every credential field the config carries — or better, go through ActiveRecord and let it assemble the connection.

The shape both share

Each depends on something true of this machine and false of the others: a test order, a trust setting. "Green locally, red in CI" is the symptom, and the cause is almost never 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.