Skip to main content

Testing Strategy: The Pyramid and the Budget

advanced28 min readLesson 159 of 169

Unit, integration, and system tests trade speed for realism โ€” allocate deliberately, and keep the suite deterministic.

The pyramid, honestly

  • Unit tests โ€” one function/class, dependencies faked. Thousands; each runs in milliseconds. They pin behavior within the code.
  • Integration tests โ€” several real pieces together (a repository against a real database, a handler with its real validation). Hundreds. They pin the seams โ€” the places unit tests structurally cannot see.
  • System/end-to-end tests โ€” the whole service from the outside. Dozens. Slow and brittle; reserve them for the critical journeys (signup, checkout, login).

Inverted pyramids (mostly E2E) are slow, flaky, and diagnose backwards. Ice-cream cones (mostly manual) aren't testing at all.

Fakes, stubs, mocks โ€” and the trade each makes

  • Stub: hard-coded answers for calls ("the clock says noon").
  • Fake: a working lightweight implementation (in-memory repository).
  • Mock: records interactions and asserts on them.

Preference order for stateful dependencies: fake first โ€” it tests behavior, survives refactors, and reads like the domain. Mocks shine for interaction contracts that have no meaningful state (did the email service get called at all?). Over-mocked suites verify the implementation, not the behavior โ€” and fail on every refactor.

Flaky tests are bugs in the test

A test that passes on retry is lying about something: time, randomness, network ordering, shared state. The standard suspects and their cures:

  • time โ€” inject a clock; never datetime.now() deep inside logic under test,
  • randomness โ€” seed it explicitly (random.Random(42)),
  • shared fixtures โ€” each test constructs its own world (factory functions),
  • order dependence โ€” if test B only passes after test A, B is broken.

pytest-randomly and -p no:cacheprovider-style hygiene make order dependence visible. A suite you cannot trust at face value is worse than no suite: it teaches the team to ignore red.

Fixtures architecture

Fixtures scale by composition, not inheritance: small fixtures (clock, empty repository) compose into bigger ones (populated world), and conftest.py shares them by directory scope. Factory functions (make_order(customer=..., total=...) with sensible defaults) beat monolithic fixtures: each test states only the facts it cares about.

Coverage: a smoke detector, not a goal

90% coverage with weak assertions measures typing effort. Cover the branches that carry risk: error paths, boundaries (empty, one, many), and every bug you have ever shipped (regression tests). Mutation testing โ€” flipping an operator and checking some test fails โ€” measures whether your assertions can actually detect change.

Now practice

Deterministic DoublesAn injected clock makes time testable; a spy port makes interaction contracts testable.2 challenges ยท ยท ~24 min