Mock vs. Fake vs. Spy: The Key Differences
While all are a form of test double-an object used to replace a real one during testing-their purpose and implementation differ significantly [1]. Understanding these differences is crucial for writing effective, maintainable tests.
-
Mock: A mock is about behavior verification. It's an object you create to replace a dependency, and its primary purpose is to record how it was used. You set expectations on the mock before your test runs and then assert on those expectations after it runs. For example, you'd use a mock to verify that a function correctly called an email client's
send()
method. The test's success depends on the mock's call history. -
Fake: A fake is a simple, working implementation of a real dependency. It has the same functionality but is much lighter and faster. A fake is used to run a more realistic test without the overhead of the real system. A classic example is using an in-memory dictionary to simulate a database. You don't verify calls on a fake; you verify that the correct state changes occurred.
-
Spy: A spy is a wrapper around a real object. Unlike a mock, which replaces a dependency entirely, a spy allows the real code to execute while simultaneously recording information about how it was called. Spies are useful when you want to verify that a method was called correctly without altering its original behavior. A spy is a combination of a stub and a mock [2].
Comparison and Use Cases
Test Double | Purpose | Key Question | Example Use Case |
---|---|---|---|
Mock | Behavior verification | Did the code call the dependency with the right arguments? | Testing if an order_confirmation() function calls the email service with the correct subject line. |
Fake | Functional testing | Did the code correctly interact with the dependency and change its state? | Testing that a save_user() function adds a new user record to an in-memory dictionary acting as a fake database. |
Spy | Observation | Did a specific method get called on a real object, and how many times? | Verifying that a log_event() method was called on a real logging object without preventing the log from being created. |
Practical Example: The Order Service
Let's imagine a simple OrderService
class with a place_order
method that performs three actions: validates the order, saves it to a database, and sends an email confirmation.
order_service.py
class OrderService:
def __init__(self, validator, db_client, email_client):
self.validator = validator
self.db_client = db_client
self.email_client = email_client
def place_order(self, order_data):
if self.validator.is_valid(order_data):
self.db_client.save(order_data)
self.email_client.send_confirmation(order_data)
return True
return False
Test with a Mock
To test that place_order
calls the email client correctly, we use a mock.
from unittest.mock import Mock
def test_place_order_sends_email_confirmation():
validator = Mock(is_valid=Mock(return_value=True))
db_client = Mock()
email_client = Mock()
service = OrderService(validator, db_client, email_client)
service.place_order({'id': '123'})
# Assert that the mock was called exactly once with the correct data
email_client.send_confirmation.assert_called_once_with({'id': '123'})
Test with a Fake
To test that place_order
correctly saves the order, we use a fake database.
class FakeDatabase:
def __init__(self):
self.orders = {}
def save(self, data):
self.orders[data['id']] = data
def test_place_order_saves_to_database():
validator = Mock(is_valid=Mock(return_value=True))
db_client = FakeDatabase()
email_client = Mock()
service = OrderService(validator, db_client, email_client)
service.place_order({'id': '456', 'amount': 100})
# Assert that the state of the fake database has changed as expected
assert db_client.orders['456']['amount'] == 100
Test with a Spy
If we wanted to verify that the save()
method on a real database client (perhaps an in-memory one for testing) was called, we could use a spy. The unittest.mock.patch.object
decorator can act as a spy.
from unittest.mock import patch
def test_place_order_calls_save_with_spy(service):
# This is a bit advanced, as it requires a real object
with patch.object(service.db_client, 'save', autospec=True) as spy_save:
service.place_order({'id': '789'})
spy_save.assert_called_once()
Final Verdict
- Use Mocks for external dependencies like email services, APIs, and logging. They are ideal for behavior verification.
- Use Fakes for lightweight implementations of internal dependencies like databases or caches. They are ideal for functional testing and are much more performant than the real thing.
- Use Spies when you need to verify an interaction with a real object without altering its behavior. They are less common in Python but can be useful for certain advanced scenarios.
This clear distinction helps you write tests that are focused, fast, and ultimately more reliable.