Test Code Standards
Tests are production code. We read them, maintain them, and rely on them as much as the code they cover. This is our reference for writing tests that are clear, fast, deterministic, and trustworthy: how to name them, how to structure them, what to assert, how to use test doubles, and what makes a test suite people trust. It works with Testing Strategy (what to test) and covers how to write the tests themselves.
A test suite is only useful if people trust it. Green should mean good, red should mean a real problem, and a failure message should tell you what broke. Flaky, slow, over-mocked, or unreadable tests destroy that trust. These standards keep tests an asset, not a cost. We use xUnit for .NET and the standard JS/TS test tools. The principles are the same for both.
Write the test to describe the behaviour. Make it fail for the right reason. Keep it independent of every other test.
Naming & structure
- DoName tests for the behaviour and the case:
Method_Scenario_ExpectedResult(for exampleOnboard_WhenScreeningTimesOut_BlocksAndEscalates). The failure name alone should tell you what broke. - DoStructure each test as Arrange, Act, Assert (Given, When, Then). Separate the parts clearly and test one logical behaviour per test.
- DoKeep each test independent. Do not share mutable state and do not depend on test order or on another test having run. Each test sets up and cleans up its own data.
- DoMake tests readable top to bottom. Extract setup helpers and builders for clarity, but keep the key inputs and the assertion visible in the test itself.
- AvoidVague names (
Test1,ItWorks), several unrelated behaviours in one test, and logic, loops, or conditionals in tests. A test with branches needs its own tests.
[Fact] public void Test1() {
var s = new Service();
Assert.NotNull(s.Do(1)); // what behaviour?
Assert.True(s.Do(2).Ok); // unrelated case
mock.Verify(m => m.Save(), Times.Once); // implementation detail
}
The name says nothing, it tests two unrelated behaviours, it has a weak not-null assertion, and it checks how the code is implemented. When it fails you learn nothing, and any refactor breaks it.
[Fact] public void Onboard_WhenScreeningTimesOut_BlocksAndEscalates() {
// Arrange
screening.Setup(s => s.CheckAsync(any)).Throws<TimeoutException>();
// Act
var result = sut.Onboard(customer);
// Assert
Assert.Equal(Decision.BlockAndEscalate, result); // never Approved
}
The name states the rule, the structure is clear, and it asserts the AML-critical outcome (fail closed). It survives refactoring, and a failure tells you exactly what broke.
Assertions
- DoAssert on behaviour and outcomes, not on implementation details, so tests survive refactoring (see Refactoring).
- DoMake assertions specific with clear failure messages. Assert the exact expected value, not just "not null". Test one focused idea per test.
- DoTest the cases that cause real bugs: error and fail-closed paths, boundaries, nulls and empties, and concurrency. Do not test only the happy path (see Testing Strategy).
- AvoidAsserting on minor details such as call order or private state, which ties the test to the implementation. Also avoid snapshot tests that nobody reviews.
Test doubles (mocks, fakes, stubs)
- DoUse test doubles to isolate the unit and to reach hard-to-test paths (for example, make a provider throw to test fail-closed). Inject dependencies to make this easy (see Inversion of Control).
- DoPrefer simple fakes and stubs for state. Verify interactions with mocks only when the interaction is the behaviour, such as "an audit record was written".
- AvoidOver-mocking, where the test only checks the mocks. At that point you are testing your assumptions, not the system (see Testing Strategy).
- DoUse a real (or close) database for data-access and tenant-scoping tests, where mocks would hide the real bug. These are integration tests.
Determinism, speed & data
- AlwaysMake tests deterministic. Inject the clock and randomness, avoid real time, network, and sleep, and fix or delete flaky tests at once. A flaky suite gets ignored.
- DoKeep unit tests fast so they run on every change. Move slow work into fewer integration and end-to-end tests (see the pyramid in Testing Strategy).
- DoUse clear, small, synthetic test data and builders. Make the relevant inputs obvious in the test.
- NeverUse real production PII or KYC data in tests or fixtures. Use synthetic or masked data only (see Test Data & Environments).
Self-review checklist
- AskDoes the test name state the behaviour and the case, and is it one behaviour per test?
- AskDoes it assert outcomes and behaviour, or is it tied to implementation details?
- AskIs it deterministic and independent? Would it pass reliably in any order?
- AskAm I over-mocking or using real PII? Are the risky paths covered, not just the happy one?