Overview
A test suite earns its cost by catching the regression you would have shipped and by documenting intended behavior. This page is the language-agnostic policy; framework mechanics live in python-testing and swift-testing. Read general-principles first.
Weight the suite like a trophy, not a pyramid
Put most effort in the middle:
- Static checks at the base: types, lint, schema validation.
- A thick integration layer: the function under test plus a real database or HTTP server.
- A few golden-path end-to-end tests: one login, one checkout.
- Unit tests reserved for tricky pure logic: a parser, a pricing rule, a date calculation.
Unit tests of glue code mostly test the doubles. A Postgres instance in a container catches bugs a mocked repository hides. See fastapi for the HTTP layer and observability for telemetry.
Treat every flaky test as a defect
A flaky test teaches the team to ignore red builds, and the real failure hides in the noise. Quarantine it on the first observed failure, file a ticket with the seed and stack trace, then land a deterministic version within a sprint or delete the test.
Make tests deterministic
Wall-clock time, network availability, and unseeded randomness will eventually fail for reasons unrelated to your code.
- Freeze time with a clock parameter or a library (
time-machine,freezegun,vi.useFakeTimers). - Stub external HTTP (
respx,msw,nock) or replay recorded cassettes. - Seed every random source; pass the generator into the function under test.
- Run tests in random order locally to expose hidden ordering coupling.
Name tests for the one rule they enforce
test_total_excludes_voided_line_items tells the reader what broke; test_calculate_total_1 does not. The failing name should make the offending diff obvious. Each test asserts one rule; several assert lines are fine when they express the same rule, and a test that checks five unrelated things should be split.
Use snapshots only for stable formats
A passing snapshot proves the output did not change, not that it is correct, and the first run freezes whatever bug existed. Snapshot stable serializations (a JSON-LD blob, a compiler IR) where a human reviews every diff. Avoid them for component trees and formatted strings that developers will update without reading.
Prefer fakes and real dependencies to mocks
A test that mocks five collaborators is testing the mocks, and the function under test probably does too much. Use an in-memory fake (FakeUserRepo) over a Mock that asserts call counts, and a real dependency in a container over any double. Use call-assertion mocks only for side effects with no observable result, such as an email send.
Set a coverage floor, never a target
Coverage shows that critical paths run at all, not that they are checked. Pick a floor your suite already clears and fail CI below it; do not gate a PR on coverage rising. The real bar is the four cases in general-principles.
Test the public contract
Test what a user, integrator, or upstream caller would notice. Tests of private helpers churn with every refactor; tests of the contract survive it. For LLM-facing systems, write evals at the contract layer (prompt input, JSON output, citations), not at every internal call. See evaluation and rag.