Testing Strategy
Tests let us change code with confidence. A good test suite gives fast feedback, catches regressions before customers do, and shows how the system should behave. A bad suite is slow, flaky, and tests the mocks instead of the behaviour. Aim your testing at risk: the logic that, if wrong, costs money, leaks data, or breaks compliance.
The strategy is the test pyramid: many fast unit tests over individual logic, fewer integration tests over how components fit together (especially data access and external boundaries), and a small number of end-to-end tests over critical journeys. Test at the lowest level you can. Use unit tests where possible, and integration tests where the interaction is the risk. This keeps the suite fast and stable.
Coverage is a means, not the goal. A high percentage of trivial getters proves little. What matters is that the high-risk behaviour is tested for the cases that cause real harm: the fail-closed path when screening errors, the tenant-isolation boundary, the idempotent retry, and the money calculation. On a regulated platform, the test suite is also evidence that critical controls behave as required.
Test the right things, the right way
- DoFollow the pyramid: many fast unit tests, fewer integration tests, few end-to-end tests. Run the fast ones on every change.
- DoTest behaviour and outcomes, not implementation details, so tests survive refactoring and describe what the code should do.
- DoCover the cases that cause harm: error and fail-closed paths, boundaries, edge cases, concurrency, and the failure paths, not just the happy path.
- DoAim your testing at risk. Money, security, tenant isolation, and compliance logic get thorough tests. Trivial code does not need ceremony.
- ConsiderWriting the test first for tricky logic (TDD), and adding a regression test for every bug so it can never quietly return.
- AlwaysAdd a test that proves each security or compliance-critical behaviour, such as fail-closed screening, tenant isolation, and authorisation, so a regression turns the build red.
[Fact] void Approves_clean_customer() {
screening.Setup(s => s.Check(any)).Returns(Clear);
Assert.Equal(Approved, sut.Onboard(customer));
}
Only the everything-works case is covered. The dangerous path, what happens when screening errors or times out, is exactly the one that must fail closed, and it is untested.
[Fact] void Escalates_when_screening_unavailable() {
screening.Setup(s => s.Check(any)).Throws();
Assert.Equal(BlockAndEscalate, sut.Onboard(customer)); // never Approved
}
The test proves the AML-critical rule: when the check cannot complete, the customer is held, never auto-approved. A regression here now fails the build.
Keep the suite trustworthy
- DoMake tests deterministic and independent: no shared state, no ordering dependence, and an injected clock and randomness, so a green run truly means green.
- DoUse realistic test data that is synthetic or masked. Never use production PII or KYC data in tests or fixtures.
- DoFix or remove flaky tests at once. A suite people do not trust gets ignored, and then it protects nothing.
- ConsiderIntegration tests against a real database (or a close equivalent) for data-access and tenant-scoping logic, where mocks would hide the real bug.
- AvoidMocking so heavily that the test only checks the mocks. At that point you are testing your assumptions, not the system.
- NeverCopy production data, especially PII, KYC, or AML data, into tests or fixtures. Use synthetic or masked data.
Self-review checklist
- AskAre the high-risk paths (money, security, tenant isolation, fail-closed) tested, not just the happy path?
- AskDo these tests verify behaviour, or just mirror the implementation and its mocks?
- AskAre they deterministic and independent — would they pass reliably in any order?
- AskIs any real PII/KYC data in my fixtures that should be synthetic?