Test Doubles
Test doubles replace real collaborators with dummies, stubs, spies, mocks, or fakes so a unit can be tested without slow or unpredictable dependencies.
Definition
A test double is any object or component that stands in for a real dependency during a test, from a dummy passed only to satisfy a signature to a fake with working behavior.
Context and scope
Used at unit and component level when dependencies are slow, non-deterministic, expensive, or difficult to provoke into specific failure states.
Common misconceptions
- Mocks and stubs are the same thing and the terms are interchangeable
- More mocking always means better isolation and a better test
- A fake must reproduce the full behavior of the production dependency
Anti-patterns
- Mocking every collaborator until the test asserts internal call sequences
- Using a hand-written fake whose behavior has drifted from the real dependency
- Verifying interactions when the observable outcome is what the caller cares about
Related concepts
Related examples
Authoritative source
Five roles
A dummy fills a parameter but is never used. A stub returns canned answers. A spy records calls for later inspection. A mock is pre-programmed with expectations and fails when they are violated. A fake is a working but simplified implementation, for example an in-memory repository. The names matter less than the intent: each replaces a real collaborator so the test can control one variable at a time.
State versus interaction
Prefer asserting on observable outcomes, because those survive refactoring. Interaction checks are appropriate when the collaboration itself is the requirement, such as “publish exactly one event after commit”. Overusing mocks couples tests to internal structure: rename a private method and dozens of tests fail even though behavior is unchanged.
When to use them
Use doubles for clocks, randomness, network clients, payment gateways, and slow storage. Use real dependencies when the risk lives in the integration itself: SQL dialect, serialization, transaction boundaries, retry semantics. Compared to a fully mocked unit test, an integration test with a real database is slower but catches a different class of defect.
The drift problem
Every fake encodes assumptions about the real system. When the real dependency changes, the fake keeps passing and confidence becomes false. Contract tests or a small set of integration tests against real infrastructure keep doubles honest. Review fakes when the dependency’s API changes.
Guidance
Mock at architectural boundaries, not between small functions. Keep doubles minimal, name them after the behavior they simulate, and assert outcomes first. If a test needs six mocks to run, the design is telling you something.