Read alongside
Software Engineering at Google: Lessons Learned from Programming Over Time: Ch. 13: Test Doubles
Test sizes and scope, maintainable unit tests, and a detailed treatment of fakes, stubbing and interaction testing with their tradeoffs (Ch. 11 to 14).
Objectives
- Derive test cases for a function by partitioning its input space and including boundaries, and explain what coverage does and does not tell you.
- Compare unit, integration, contract and end-to-end tests by speed, fidelity, isolation and maintenance cost, and place a given risk at the cheapest level that catches it.
- Distinguish dummies, stubs, spies, mocks and fakes, and explain how over-mocking couples tests to implementation.
- Write a property-based test with at least two properties and use a shrunk counterexample to locate a bug.
- Turn a bug report into a regression test that fails before the fix and passes after it, at the lowest level that reproduces the bug.
- Diagnose common sources of flaky tests and propose fixes.
Why it matters
Tests are how a team changes code without fear. A good suite fails when behavior breaks and stays quiet during refactoring; a bad one is slow, flaky or so tied to implementation details that every change means rewriting tests. In coding interviews, "how would you test this?" is a natural follow-up to any solution, and naming edge-case partitions before running anything is a visible signal of care. In design and project discussions, interviewers ask how you verified a system that depends on a database, a payment provider or a language model, which is where choices about test doubles and contract tests matter.
Core ideas
Choose cases systematically. Partition each input into classes that should behave alike (empty, one, many; negative, zero, positive; below, at and above a limit), pick one case per class, and add the boundaries between classes. Coverage tools show what you never executed, which is useful, but executed is not the same as checked: a test without meaningful assertions covers lines and verifies nothing.
Levels. Unit tests exercise a small piece in isolation and are fast and precise. Integration tests exercise real collaborators, such as a database in a container or a real HTTP stack, and catch wiring, serialization and query bugs that unit tests cannot. End-to-end tests drive the deployed system through its interface, with the highest fidelity and the highest cost and flakiness. Software Engineering at Google classifies tests by size and scope and argues for a portfolio dominated by small, narrowly scoped tests. Place each risk at the cheapest level that catches it: business rules in unit tests, SQL and migrations in integration tests, a few critical journeys end to end.
Contracts. Where two teams or services meet at an API, a contract test checks that the provider still satisfies what consumers rely on. In consumer-driven contracts each consumer publishes its expectations and the provider runs them in its own build, so a breaking change fails before deployment rather than in production.
Test doubles. In the vocabulary Gerard Meszaros introduced and Fowler popularized: a dummy only fills a parameter, a stub returns canned answers, a spy records calls, a mock is programmed with expected interactions and fails if they do not happen, and a fake is a working lightweight implementation such as an in-memory repository. Stubs and fakes support state verification ("afterwards the order is paid"); mocks verify interactions ("charge was called once with these arguments"). The risks: a mock encodes your belief about a dependency, so tests can pass while the real dependency behaves differently; interaction checks break under harmless refactoring; deep mock setups hide design problems. Prefer real implementations when they are fast and deterministic, then fakes (ideally maintained by the dependency's owners and checked against the real thing), and keep mocks for interactions that are themselves the behavior, such as "send exactly one email".
Properties. Property-based testing generates many inputs and checks statements that must hold for all of them: decoding an encoding returns the original, sorting yields an ordered permutation, an optimized function agrees with a brute-force oracle. When a property fails, the library shrinks the input to a minimal counterexample. Stateful components can be tested the same way by generating operation sequences and comparing against a simple model.
@given(st.lists(st.tuples(st.integers(0, 50), st.integers(0, 50))))
def test_merge_intervals(raw):
intervals = [(min(a, b), max(a, b)) for a, b in raw]
out = merge(intervals)
assert all(out[i][1] < out[i + 1][0] for i in range(len(out) - 1))
assert points(out) == points(intervals) # same covered points
Regression tests. When a bug is reported, first write a test that reproduces it and fails, then fix the code and watch it pass. Write it at the lowest level that still reproduces the bug so it stays fast and points at the cause.
Common misconceptions
- "High coverage means well tested." Coverage measures execution, not verification; reviewing assertions or mutation testing gives a better signal.
- "Mocks make unit tests better." Over-mocking tests the call sequence, breaks on harmless refactors and keeps passing when the real dependency changes.
- "Integration tests are too slow to bother with." Containerized databases and in-process servers make many integration tests run in seconds, and they catch the serialization, transaction and query bugs unit tests miss.
- "Flaky tests are noise; rerun until green." Flakiness often signals real races, time dependence or order coupling; quarantine, track and fix it.
- "Property-based testing is only for pure math functions." Generated operation sequences compared against a model test caches, queues and stores well.
How to approach it
Read the MIT 6.102 testing reading for partitioning, then Software Engineering at Google Ch. 11 to 13 and Fowler's "Mocks Aren't Stubs". Practice on code you already have: a partition table, a property-based test against a brute-force oracle, and a fake for one external dependency. In interviews, mention tests before you are asked: list partitions while clarifying, walk two or three cases by hand after coding, and say which tests you would automate. Graduate candidates should produce good cases; mid-level candidates are expected to discuss levels and doubles; senior candidates are asked how they keep a large suite fast, deterministic and trusted across teams.
Self-check before you finish
- Why is 100% line coverage compatible with a badly tested function?
- When is a fake preferable to a mock, and how do you keep a fake honest?
- What does a consumer-driven contract test catch that provider unit tests do not?
- Name three properties you could test for a JSON serializer.
- What are the common causes of flaky tests, and how would you find which one applies?
- At which level would you test a SQL query that aggregates per tenant, and why?