Unit 2 · Software engineering · Lesson 4 of 15
Testing strategy
Step 1: Learn the ideas
- Partitioning, boundaries and test-first work
- Unit, integration and end-to-end tests
- Contract tests and consumer-driven contracts
- Test doubles: dummies, stubs, spies, mocks and fakes
- State verification versus interaction verification
- Property-based, differential and model-based testing
- Regression tests from bug reports
- Coverage as a diagnostic rather than a goal
- Flaky tests: time, randomness, ordering and concurrency
- Testing code that calls nondeterministic services such as language models
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.
Suggested order
- 1Read MIT 6.102 “02: Testing” and write a partition table, with boundaries, for a function you wrote recently.
- 2Read Software Engineering at Google Ch. 11 (Testing Overview) and Ch. 12 (Unit Testing) on test sizes, scope and what keeps unit tests maintainable.
- 3Read Software Engineering at Google Ch. 13 (Test Doubles) and Fowler’s “Mocks Aren’t Stubs”; classify every double in one of your test suites and replace one mock with a fake or the real implementation.
- 4Read “What is Property Based Testing?” on hypothesis.works, then write properties for Merge Intervals and a differential test comparing your LRU Cache with a simple list-based model.
- 5Read Ian Robinson’s “Consumer-Driven Contracts” and sketch the contract a client of one of your APIs would publish.
- 6Take a past bug, yours or from a public issue tracker, and write the regression test that would have caught it at the lowest possible level.
Step 2: Check off what you can do
Tap each one you can genuinely do or explain. Each earns 10 XP.
Step 3: Go deeper
Software Engineering at Google: Lessons Learned from Programming Over TimeStart here
Abseil (free HTML edition of the O'Reilly book)Section: 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).
Open(opens in a new tab)Link responded, 5 Oct 2026MIT 6.102 Software Construction (readings)Start here
MIT EECSSection: 02: Testing
Systematic case selection by partitioning and boundaries, coverage, unit versus integration tests and automated regression testing.
Open(opens in a new tab)Link responded, 5 Oct 2026Mocks Aren't Stubs
martinfowler.comThe classic distinction between state and behavior verification and the shared vocabulary for test doubles.
Open(opens in a new tab)Link responded, 5 Oct 2026What is Property Based Testing?
Hypothesis (hypothesis.works)Short introduction to property-based testing: generated inputs, properties and shrinking, compared with example-based tests.
Open(opens in a new tab)Link responded, 5 Oct 2026Consumer-Driven Contracts: A Service Evolution Pattern
martinfowler.comHow consumer expectations become provider-side tests, so an API change that breaks a consumer fails in the provider’s build.
Open(opens in a new tab)Link responded, 5 Oct 2026
Step 4: Practice
Use these problems as systems under test rather than puzzles. Solve each, or reuse an earlier solution, then write a partition-based example suite, at least two properties, and a differential test against a deliberately simple reference model such as a sorted list or a linear scan. For the stateful designs, generate random operation sequences, compare every result with the model, and keep each shrunk counterexample as a permanent regression test.
- Merge IntervalsLeetCode(opens in a new tab)Not attemptedLeetCode
- LRU CacheLeetCode(opens in a new tab)Not attemptedLeetCode
- Time Based Key-Value StoreLeetCode(opens in a new tab)Not attemptedLeetCode
- Insert Delete GetRandom O(1)LeetCode(opens in a new tab)Not attemptedLeetCode
Record attempts on each problem’s page (+15 XP, +10 more when solved on your own).
Step 5: Test yourself
Answer each one out loud or on paper before you finish the lesson.
- Question 1Why is 100% line coverage compatible with a badly tested function?
- Question 2When is a fake preferable to a mock, and how do you keep a fake honest?
- Question 3What does a consumer-driven contract test catch that provider unit tests do not?
- Question 4Name three properties you could test for a JSON serializer.
- Question 5What are the common causes of flaky tests, and how would you find which one applies?
- Question 6At which level would you test a SQL query that aggregates per tenant, and why?
You’re done when you can
- Produce a partition table with boundaries for an unseen function in under ten minutes.
- Explain the difference between a stub, a fake and a mock, with one risk of each, in under two minutes.
- Write a property-based or differential test that finds a planted bug in one of your own solutions.
- Describe a test portfolio (unit, integration, contract, end-to-end) for a service that calls a database and a third-party API, with a reason for each level.
- Turn a bug report into a failing regression test before changing any code.
Finish the lesson
Unlocks next: Debugging, code reading, review and refactoring, Deployment, observability, rollout and schema migration, Research coding: prototypes, reference checks and instrumentation


