Definition
Test Double
A Test Double is a generic term for any case where you replace a production object for testing purposes. It is analogous to a “stunt double” in a movie.
Goal: Isolate the system under test from its dependencies to ensure tests are fast, deterministic, and focused.
Motivation
Real software often depends on infrastructure that is unsuitable for unit testing:
- Speed: External dependencies (Databases, Web Services) are slow.
- Determinism: Some behaviours are hard to reproduce (e.g.,
NetworkTimeout, specific timestamps). - Control: It is difficult to force a real external system into an error state.
Types
There are five common types of doubles, increasing in complexity:
Dummy Object
Dummy Object
Objects that are passed around but never actually used. They are usually just placeholders to fill parameter lists.
- Purpose: Satisfy API signatures.
- Behaviour: None.
def test_customer_count():
# The email_service is a dummy;
# it's required by the constructor
# but not used in this test.
dummy_email_service = None
customer_repo = CustomerRepository(dummy_email_service)
assert customer_repo.count() == 02. Fake Objects
Fake Object
Objects that have working implementations, but take shortcuts which make them unsuitable for production (e.g., using an in-memory
HashMapinstead of a real database).
- Purpose: Simulate the real system behaviour quickly.
- Behaviour: Functional but simplified.
class FakeDatabase:
def __init__(self):
self.users = {}
def save(self, user):
self.users[user.id] = user
def find(self, user_id):
return self.users.get(user_id)Stub
Stub
Objects that provide canned answers to calls made during the test. They generally do not respond to anything outside what’s programmed for the specific test.
- Purpose: Provide specific inputs to the system under test.
- Behaviour: Hard-coded return values.
class StubWeatherService:
def get_temperature(self, city):
return 25 # Canned answer
def test_temperature_display():
controller = WeatherController(StubWeatherService())
assert controller.display_temp() == "25°C"Mock
Mock
Objects pre-programmed with expectations which form a specification of the calls they are expected to receive. They throw exceptions if they receive a call they don’t expect.
- Purpose: Verify the interaction between objects.
- Behaviour: Logs calls and validates them against specific expectations.
from unittest.mock import Mock
def test_order_emails_customer():
mailer = Mock()
order = Order(mailer)
order.checkout()
# Verify interaction
mailer.send_email.assert_called_with("Thanks for your order!")Spy
Spy
Wrappers around real (or stubbed) objects that log interactions (arguments, call counts) for later verification.
- Purpose: verify side effects without mocking the entire behaviour.
- Behaviour: Acts like the real object but records usage.
class SpyLogger:
def __init__(self):
self.messages = []
def log(self, message):
self.messages.append(message)
def test_service_logs_error():
spy = SpyLogger()
service = Service(spy)
service.do_something_risky()
assert "Error occurred" in spy.messagesTrade-offs
| Aspect | Pros | Cons |
|---|---|---|
| Control | Allows testing of edge cases (errors, timeouts) impossible with real systems. | |
| Speed | Simulations are orders of magnitude faster than I/O. | |
| Coupling | Tests may become coupled to implementation details (interaction testing). | |
| Fidelity | Doubles are not real; passing tests does not guarantee production success (validation gap). | |
| Maintenance | Doubles must be updated whenever the real interfaces change. |