Why We Need Test Doubles
A test double is any object that stands in for a real dependency during a test — a database connection, a payment gateway, an email service, or the system clock. Real dependencies are often slow, non-deterministic, expensive to call repeatedly, or simply unavailable in a CI environment, so substituting a lightweight, controllable double lets a unit test stay fast and isolated per the FIRST principles. The term 'test double' comes from film production's stunt doubles: the double stands in for the real actor in a specific, controlled scene where the real thing would be risky or impractical to use directly.
Cricket analogy: A bowling machine set to reproduce a specific fast bowler's line and length stands in for the actual bowler during a batting drill, giving repeatable, controllable deliveries the way a test double gives repeatable, controllable responses.
Dummies, Stubs, and Spies
A dummy is passed around to satisfy a parameter list but is never actually used — for example, passing an empty object as a required 'logger' argument that the code under test never calls. A stub goes further: it returns pre-programmed, canned answers to calls made during the test, such as a stubbed UserRepository.findById() that always returns a fixed user object regardless of the id passed in, letting you test logic that depends on that data without touching a real database. A spy wraps a real or stubbed object and records how it was called — arguments, call count, call order — so the test can later assert on that interaction, such as verifying that emailService.send() was called exactly once with the correct recipient.
Cricket analogy: A dummy is like a substitute fielder standing at deep fine leg purely to satisfy the eleven-player rule when no ball is ever hit there, present but functionally irrelevant to the drill being run.
from unittest.mock import Mock
def test_order_confirmation_sends_email():
# Mock: pre-programs behavior AND lets us verify the interaction
email_service = Mock()
order_processor = OrderProcessor(email_service=email_service)
order_processor.confirm(order_id=42, customer_email="a@example.com")
# Mock-style assertion: verify the call happened as expected
email_service.send.assert_called_once_with(
to="a@example.com",
subject="Order #42 confirmed"
)
def test_discount_uses_stubbed_pricing_table():
# Stub: just returns canned data, no interaction verification
pricing_table = Mock()
pricing_table.get_rate.return_value = 0.10
total = calculate_price(subtotal=200, pricing_table=pricing_table)
assert total == 180Mocks vs. Fakes
A mock is a test double pre-programmed with expectations about how it should be called, and the test explicitly verifies those expectations — for example, expecting emailService.send() to be called exactly once, and failing the test if it's called zero or two times. This is 'interaction-based' testing, distinct from a stub, which is purely 'state-based' and never asserts on how it was used. A fake, by contrast, is a real working implementation, just a simplified one unsuitable for production — an in-memory HashMap-backed UserRepository that actually stores and retrieves data correctly, satisfying the same interface as the real database-backed repository but without needing a live database. Fakes are more expensive to build than stubs but produce more realistic tests, since the logic under test interacts with genuinely working behavior rather than canned answers.
Cricket analogy: A mock is like a coach who specifically counts whether the bowler executed exactly six yorkers in an over and fails the drill otherwise, checking the interaction pattern rather than just the outcome.
Prefer stubs and fakes for verifying state (what the code returns or stores) and reserve mocks for verifying genuinely important side effects, like 'did we actually call the payment gateway.' Overusing interaction-based mocks for everything creates brittle tests that break on any refactor of internal call patterns, even when behavior is correct.
Excessive mocking of collaborators that are themselves being redesigned, or mocking so much of a class's dependencies that the test no longer exercises any real logic, is a common anti-pattern known as 'mock overload.' If a test mocks five out of six calls a method makes, ask whether an integration test with a real or fake dependency would give more confidence.
- Test doubles substitute slow, unpredictable, or unavailable real dependencies with controllable stand-ins.
- A dummy is passed but never actually used by the code under test.
- A stub returns pre-programmed canned answers and supports state-based assertions.
- A spy records how it was called so the test can assert on that interaction afterward.
- A mock is pre-programmed with expectations and fails the test if those interactions don't occur as specified.
- A fake is a real, simplified working implementation, like an in-memory repository, rather than canned data.
- Overusing mocks for every collaborator creates brittle, implementation-coupled tests — reserve them for meaningful side effects.
Practice what you learned
1. What is the key difference between a stub and a mock?
2. Which test double is a genuinely working, simplified implementation of a dependency, such as an in-memory repository?
3. A test double that is passed as a required argument but is never called by the code under test is called a:
4. What common anti-pattern occurs when a test mocks nearly every collaborator a method calls?
5. What does a spy record that a plain stub does not?
Was this page helpful?
You May Also Like
Writing Good Unit Tests
Learn what separates a fast, trustworthy unit test from a brittle one, and the core principles (FIRST, AAA) that guide good test design.
Assertion Styles and Best Practices
Compare classic, fluent, and matcher-based assertion styles and learn practical rules for writing assertions that fail with useful messages.
Test Coverage Explained
Understand how line, branch, and path coverage are measured, why 100% coverage doesn't guarantee correctness, and how to use coverage reports wisely.
Related Reading
Related Study Notes in Software Engineering
Browse all study notesMicroservices Study Notes
Software Architecture · 30 topics
Software EngineeringDesign Patterns Study Notes
Software Design · 30 topics
Software EngineeringSoftware Engineering Study Notes
Python · 40 topics
Software EngineeringGit & Version Control Study Notes
Bash · 40 topics
Software EngineeringSystem Design Study Notes
Architecture · 40 topics