Testing Strategy
We test what is risky and what crosses a boundary. The gate is the diff, not the total — a percentage on a whole codebase punishes legacy code and rewards padding.
This page owns test policy. The tool for each stack lives on that stack’s page.
The lanes
Section titled “The lanes”flowchart LR PR["Pull request"] --> Fast["Fast lane<br/>unit + no-Docker subset"] PR --> Int["Integration lane<br/>real database"] Fast --> Merge["Merge to main"] Int --> Merge Merge --> E2E["E2E lane<br/>critical journeys"] E2E --> Rel["Release"] Merge --> Night["Nightly<br/>slow / perf probes"] Night --> WI["Opens a work item"]
| Lane | Runs on | Blocks |
|---|---|---|
| Fast — unit and the no-Docker subset | Every PR, no path filter | Merge |
| Integration — real database, real host | Every PR | Merge |
| E2E — critical journeys | Merge to main, plus nightly |
Release |
| Slow — perf and scale probes | Nightly | Nothing; opens a work item |
- A lane skipped by a path filter is a lane that does not exist. Every PR runs Fast and Integration.
- Lanes block because a Build Validation branch policy on
mainrequires them. The pipeline YAML alone blocks nothing — see Pipelines & Environments.
Integration-first for the backend. Unit-test pure computation; test anything crossing a boundary — HTTP, the database, auth — through a real host against a real database. That is now roughly as cheap as a mock-heavy unit test and it catches the wiring bugs unit tests structurally cannot. Mobile stays pyramid-shaped — device UI tests are still expensive.
What to test where
Section titled “What to test where”| Layer | Target | Runner |
|---|---|---|
| Unit | Pure business logic, domain rules, calculations | xUnit v3, Vitest, JUnit, Swift Testing |
| Integration | Endpoint + real database, EF queries, external adapters | WebApplicationFactory + a real container |
| E2E | A few critical journeys only (login, and the core flow) | Playwright, Compose UI test, XCUITest |
We do not test: DTO mapping with no rules, framework configuration, third-party library behaviour, generated code, or markup. If a test can only fail when the compiler is broken, delete it.
The coverage gate
Section titled “The coverage gate”Every line a pull request adds or changes must be ≥ 80% covered. Total coverage is published, never blocking.
- Each repo emits Cobertura from its own runner, and the pipeline compares against the merge base. Diff coverage needs a full-history checkout — a default shallow clone silently passes.
- “Business logic only” is a filter, not an intention: the committed coverage settings exclude migrations, DTOs, generated code and startup wiring;
[ExcludeFromCodeCoverage]marks the rest. The filter lives in committed config, not on this page.
Determinism
Section titled “Determinism”A flaky test is a design defect, not bad luck.
| Rule | Mechanism |
|---|---|
| No wall clock in logic | Inject TimeProvider; tests use a fake clock and advance it |
No Thread.Sleep or fixed waitFor(ms) |
Await a condition, or advance the fake clock |
| Cancellation is threaded through | Every async test call passes the test’s cancellation token |
| Test inputs are built by a seeded faker | One faker per aggregate — unseeded randomness is a flaky test with a delayed fuse |
Flaky tests
Section titled “Flaky tests”- Retries are banned in the Fast and Integration lanes. A non-deterministic assertion is a bug.
- The E2E lane runs one retry with a trace on first retry. Retrying is how we capture the trace, never how we go green. A retry-pass is reported as flaky and opens a work item.
- A flaky test is quarantined the same day — skipped, with the work item linked. The fix budget is one sprint.
Who owns what
Section titled “Who owns what”- Developers write every automated test — unit, integration, E2E — in the same PR as the change. Never a follow-up story.
- QA owns exploratory and manual regression in Azure Test Plans, and signs the release. QA does not write, own, or repair automated tests.
- A bug found in production gets a failing test first, then the fix — see Delivery Lifecycle.
- Static analysis and AI review comment on PRs; they never block, and they are not tests — see the gate list.