Architect
Standards
ReferenceNormative

Testing

Integration tests against real infrastructure are the gate; unit tests cover the rules that need no infrastructure. Mock-heavy adapter tests assert that the mocks were configured correctly.

Verified 2026-07-31 · Stack-independent. The Go container harness below was re-measured 2026-08-12.

Integration tests against real infrastructure are the gate; unit tests cover the rules that don't need infrastructure.

Mock-heavy "unit" tests of adapters assert that the mocks were configured correctly, and pass while production is broken — the classic green-suite, red-service failure. Containers made real dependencies cheap enough that mocking them is now a choice, and usually the wrong one.

The split

  • Integration tests — real database, real broker, real cache, via Testcontainers or the ecosystem equivalent. One shared base class or fixture wires the containers, an HTTP client, a test auth setup, a recording event publisher and a fake-data generator. Everything cross-process — HTTP contracts, persistence, transactions, events — is proven here.
  • Unit tests — domain rules, state transitions, validators, mappers, filters, serialisation, error branches. Fast, no container, and no mocking framework needed because the core has no I/O to mock. That is a payoff of hexagonal structure, not a separate discipline.
  • Functional / acceptance features (Gherkin) — written so a scenario reads without opening code. They belong to their own module or feature, boot that slice alone, and exercise only that slice's public API. No central e2e module — it becomes everyone's dumping ground and nobody's responsibility.

Rules that hold the line

  • A feature file is the specification, so its runner ships disabled. A scenario is enabled by the change that makes it pass — never before. Otherwise the suite is permanently red and everyone learns to ignore it.
  • Every endpoint needs coverage of four things: success, validation failure, authentication/authorisation failure where applicable, and each business conflict it can actually produce. A happy-path-only test is a smoke test wearing a suit.
  • Assert the generated API document, not just the responses. A documentation annotation is only safe to delete if a test demonstrably covers what replaced it.
  • Never infer a green build. Run it, read the output, and report what passed and what was skipped. See evidence.
  • Conventions get architecture tests, not documentation. ArchUnit, dependency-cruiser, eslint-boundaries, import-linter. Add the rule in the same commit as the convention.
  • A flaky test is a broken test. Quarantine and fix — never retry until green. Retries train the team to distrust every failure.

What the architecture lens adds

  • Every rule has an observable. The module reference states, for each requirement, what someone must be able to check. That sentence is the test's assertion, written before the test and in business language.
  • Tests cite requirement identifiers in their names. That citation is what makes coverage a query rather than an opinion, and it is the same join the work register uses.
  • Reversal cost decides depth. A test guarding something expensive to reverse — stored shape, money, confidentiality — earns real setup. One guarding something replaceable behind an interface you own does not.
  • One check that can fail is worth more than five that cannot. See verify in proportion to the blast radius.

Traps

Container startup dominating the suite → one reusable container set per run, not per class. Reuse mode locally, fresh in CI.

Test binaries owning infrastructure — per-package testcontainers with reuse-by-name, database-per-test cloning — produce provisioning races, stale reused state, and cached test results that hide real red.

The cure that held (measured 2026-08-12, Go + Postgres: 325s red → 30s green). One top-level harness owns the container and the single logical database, migrates once, and hands tests a URL. The fixture refuses Docker and refuses any non-test database by name.

  • Transaction-per-test rollback via go-txdb — nested Begin/Commit become savepoints — for ordinary tests.
  • A private schema with search_path and application_name URL parameters for committed, reconnect and lock-contention tests.

Two traps inside the cure: sequences do not roll back, so never assert literal serial ids; and concurrent packages speculatively inserting the same seed primary keys deadlock (40P01) — serialise database packages until seeds are pre-committed and a stress gate proves otherwise.

Tests sharing state through the database → each test owns its data and generates unique values. Do not rely on a seeded fixture nobody dares change.

Asserting on log output or wall-clock time → both are flake factories. Assert on recorded events and injected clocks instead.

On this page