Skip to content

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.

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 main requires 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.

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.

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.

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
  • 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.
  • 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.

See also: Examples · .NET · Angular · Mobile.

👤 Owner: Aylin Koyuncu🗓 Last reviewed: 2026-07-26

اقرأ هذه الصفحة بالعربية