A pytest test double isn’t just a stand-in; it’s a meticulously crafted illusion designed to isolate the unit under test by providing predictable, controlled responses to its dependencies.
Let’s see this in action. Imagine we’re testing a UserService that fetches user data from a UserRepository.
# --- real code ---
class UserRepository:
def get_user_by_id(self, user_id: int) -> dict:
# In a real app, this would hit a database
print(f"Real DB: Fetching user {user_id}")
if user_id == 1:
return {"id": 1, "name": "Alice", "email": "alice@example.com"}
return {}
class UserService:
def __init__(self, user_repo: UserRepository):
self.user_repo = user_repo
def get_user_greeting(self, user_id: int) -> str:
user_data = self.user_repo.get_user_by_id(user_id)
if not user_data:
return "User not found."
return f"Hello, {user_data['name']}!"
# --- test code ---
import pytest
from unittest.mock import MagicMock
# Using a stub
class StubUserRepository:
def __init__(self, user_data_to_return: dict):
self._user_data_to_return = user_data_to_return
def get_user_by_id(self, user_id: int) -> dict:
print(f"Stub: Returning data for user {user_id}")
return self._user_data_to_return
def test_user_service_greeting_with_stub():
stub_repo = StubUserRepository({"id": 2, "name": "Bob", "email": "bob@example.com"})
user_service = UserService(stub_repo)
assert user_service.get_user_greeting(2) == "Hello, Bob!"
# Using a fake (more complex behavior, but still controlled)
class FakeUserRepository:
def __init__(self):
self._users = {}
def add_user(self, user_data: dict):
self._users[user_data["id"]] = user_data
def get_user_by_id(self, user_id: int) -> dict:
print(f"Fake DB: Looking up user {user_id}")
return self._users.get(user_id, {})
def test_user_service_greeting_with_fake():
fake_repo = FakeUserRepository()
fake_repo.add_user({"id": 3, "name": "Charlie", "email": "charlie@example.com"})
user_service = UserService(fake_repo)
assert user_service.get_user_greeting(3) == "Hello, Charlie!"
assert user_service.get_user_greeting(99) == "User not found."
# Using unittest.mock.MagicMock (often the most practical)
def test_user_service_greeting_with_mock():
mock_repo = MagicMock(spec=UserRepository) # spec ensures it has the same methods
mock_repo.get_user_by_id.return_value = {"id": 4, "name": "David", "email": "david@example.com"}
user_service = UserService(mock_repo)
assert user_service.get_user_greeting(4) == "Hello, David!"
mock_repo.get_user_by_id.assert_called_once_with(4) # verify interaction
# Testing the "not found" case with a mock
def test_user_service_greeting_not_found_with_mock():
mock_repo = MagicMock(spec=UserRepository)
mock_repo.get_user_by_id.return_value = {} # Simulate user not found
user_service = UserService(mock_repo)
assert user_service.get_user_greeting(99) == "User not found."
mock_repo.get_user_by_id.assert_called_once_with(99)
This demonstrates how StubUserRepository and FakeUserRepository provide predefined responses, allowing UserService to be tested in isolation. The MagicMock from unittest.mock (which pytest integrates seamlessly) is the most common tool here, letting you define return values and assert calls without writing explicit classes.
The core problem test doubles solve is non-determinism and side effects in dependencies. Real databases, network calls, or complex services are slow, unreliable, and can alter external state. By replacing these with controlled doubles, your tests become fast, predictable, and focused solely on the logic of the component you’re testing.
- Stubs are the simplest. They are programmed to provide canned answers to calls made during the test. You pre-define exactly what
get_user_by_idwill return for a given input. They are about state verification – ensuring the correct data is passed to the component under test. - Fakes are more functional implementations of a dependency. They have working logic, but are simplified (e.g., an in-memory database instead of a real one). They are useful when the dependency’s behavior itself is complex and you want to test interactions with a simulated but functional version. They are about behavior verification – ensuring the component under test calls the dependency correctly.
- Mocks are a hybrid. They can act as stubs (returning predefined values) and also record interactions (like how many times a method was called and with what arguments). This allows for both state and behavior verification.
unittest.mock.MagicMockis the ubiquitous Python mock object.
The mental model is this: your UserService depends on UserRepository. When testing UserService, you don’t want to depend on the actual UserRepository because it’s slow, might not have the right data, or might have side effects. So, you substitute it with something that looks like a UserRepository to UserService but behaves exactly how you want for the test.
When you use MagicMock(spec=UserRepository), you’re telling the mock object to behave like a UserRepository but to also raise an AttributeError if you try to access or call something that doesn’t exist on the real UserRepository. This is crucial for catching typos in your test code or misunderstandings of the dependency’s interface. It’s less about the mock knowing how to do the real work and more about it knowing what methods and attributes the real object has.
The most surprising thing is how often you can get away with just stubbing, even for complex scenarios. People often over-engineer fakes when a simple return_value on a MagicMock would suffice. The key is to identify what specific data or behavior your unit needs from its dependency to execute its logic. Don’t mock what you don’t need.
The next concept you’ll likely grapple with is when and how to use mocks to verify interactions versus just stubbing for state.