Mocking
In this chapter, we'll learn about mocking - a powerful testing technique. We'll cover:
- What is mocking and when to use it
- Python's
unittest.mockmodule Mock,MagicMock, andpatch- Using
pytest-mockfor cleaner syntax - Best practices for mocking
- Testing previously untested code with mocks
What is mocking?
Mocking replaces real objects with controlled substitutes during testing. This is useful when:
- Real dependencies are slow (databases, APIs)
- Real dependencies have side effects (sending emails, charging credit cards)
- You want to test error conditions that are hard to trigger
- You want to isolate the code under test
Python's unittest.mock
The unittest.mock module is part of Python's standard library.
Basic Mock usage
from unittest.mock import Mock
# Create a mock object
mock = Mock()
# Call it like a function
result = mock(1, 2, 3)
# Access attributes
mock.some_attribute
# Mock returns another Mock by default
assert isinstance(mock(), Mock)
assert isinstance(mock.method(), Mock)
Configuring return values
from unittest.mock import Mock
mock = Mock()
mock.return_value = 42
assert mock() == 42
assert mock("ignored", "arguments") == 42
Setting up method return values
mock = Mock()
mock.get_name.return_value = "Alice"
mock.calculate.return_value = 100
assert mock.get_name() == "Alice"
assert mock.calculate() == 100
Testing with Mock
Write the test first
Let's test a user service that fetches data from a database:
from unittest.mock import Mock
from user_service import UserService
def test_get_user_full_name():
# Create a mock database
mock_db = Mock()
mock_db.find_user.return_value = {"first": "Alice", "last": "Smith"}
# Inject the mock
service = UserService(mock_db)
# Test the service
result = service.get_full_name(user_id=123)
assert result == "Alice Smith"
mock_db.find_user.assert_called_once_with(123)
Implementation
class UserService:
def __init__(self, database):
self.db = database
def get_full_name(self, user_id):
user = self.db.find_user(user_id)
return f"{user['first']} {user['last']}"
Assertions on mocks
Mocks record how they were called:
from unittest.mock import Mock
mock = Mock()
mock(1, 2, 3, key="value")
# Verify the call
mock.assert_called()
mock.assert_called_once()
mock.assert_called_with(1, 2, 3, key="value")
mock.assert_called_once_with(1, 2, 3, key="value")
# Check call history
assert mock.call_count == 1
assert mock.call_args == ((1, 2, 3), {"key": "value"})
Testing call sequences
from unittest.mock import Mock, call
mock = Mock()
mock.add(1)
mock.add(2)
mock.add(3)
# Verify sequence of calls
mock.add.assert_has_calls([call(1), call(2), call(3)])
MagicMock
MagicMock is a subclass of Mock that implements magic methods:
from unittest.mock import MagicMock
# MagicMock supports len(), iter(), etc.
mock = MagicMock()
mock.__len__.return_value = 5
assert len(mock) == 5
Using MagicMock as a context manager
mock = MagicMock()
mock.__enter__.return_value = "entered"
with mock as value:
assert value == "entered"
The patch decorator
patch temporarily replaces an object with a mock:
from unittest.mock import patch
# As a decorator
@patch('module.ClassName')
def test_something(MockClass):
instance = MockClass.return_value
instance.method.return_value = 42
# Your test code here
# As a context manager
def test_something_else():
with patch('module.ClassName') as MockClass:
instance = MockClass.return_value
instance.method.return_value = 42
# Your test code here
Patching where it's used
Important: Patch where the object is used, not where it's defined:
# user_service.py
from database import Database
class UserService:
def __init__(self):
self.db = Database() # Creates Database instance
# test_user_service.py
# Patch where Database is used (in user_service), not where it's defined
@patch('user_service.Database')
def test_user_service(MockDatabase):
mock_db = MockDatabase.return_value
mock_db.find_user.return_value = {"name": "Alice"}
service = UserService()
# ...
pytest-mock
The pytest-mock plugin provides a cleaner API:
# Install: pip install pytest-mock
def test_with_mocker(mocker):
# mocker.patch is like unittest.mock.patch
mock_db = mocker.patch('user_service.Database')
mock_db.return_value.find_user.return_value = {"name": "Alice"}
service = UserService()
# ...
spy()
Spy on a real object:
def test_spy(mocker):
calculator = Calculator()
# Spy wraps the real method
spy = mocker.spy(calculator, 'add')
result = calculator.add(1, 2)
assert result == 3 # Real result
spy.assert_called_once_with(1, 2) # Verify call
Testing exceptions
Configure mocks to raise exceptions:
from unittest.mock import Mock
def test_handles_database_error():
mock_db = Mock()
mock_db.find_user.side_effect = ConnectionError("Database unavailable")
service = UserService(mock_db)
with pytest.raises(ServiceError, match="Could not fetch user"):
service.get_full_name(123)
Multiple side effects
mock = Mock()
# Return different values on successive calls
mock.side_effect = [1, 2, 3]
assert mock() == 1
assert mock() == 2
assert mock() == 3
# Or call a function
mock.side_effect = lambda x: x * 2
assert mock(5) == 10
Testing previously untested code
When adding tests to existing code, mocking helps isolate dependencies.
Example: Untested email sender
# email_sender.py (existing, untested code)
import smtplib
def send_welcome_email(user_email, username):
server = smtplib.SMTP('smtp.example.com', 587)
server.starttls()
server.login('app@example.com', 'password')
message = f"Welcome {username}!"
server.sendmail('app@example.com', user_email, message)
server.quit()
return True
Adding tests with patch
from unittest.mock import patch, MagicMock
from email_sender import send_welcome_email
@patch('email_sender.smtplib.SMTP')
def test_send_welcome_email(MockSMTP):
mock_server = MagicMock()
MockSMTP.return_value = mock_server
result = send_welcome_email('alice@example.com', 'Alice')
assert result is True
MockSMTP.assert_called_once_with('smtp.example.com', 587)
mock_server.starttls.assert_called_once()
mock_server.sendmail.assert_called_once_with(
'app@example.com',
'alice@example.com',
'Welcome Alice!'
)
mock_server.quit.assert_called_once()
When to mock and when not to
Good use cases
- External APIs and network calls
- Databases when testing business logic
- File system operations
- Time-dependent operations
- Expensive computations
Avoid mocking
- Simple data structures
- Pure functions
- Code you own and control (prefer real implementations)
- Too many internals (brittle tests)
Best practices
1. Mock at the boundary
Mock external systems, not internal classes:
# Good: Mock the external API
@patch('weather_service.requests.get')
def test_weather_service(mock_get):
mock_get.return_value.json.return_value = {"temp": 20}
service = WeatherService()
assert service.get_temperature("London") == 20
# Bad: Mock internal helpers
@patch('weather_service.parse_response')
def test_weather_service(mock_parse): # Don't do this
pass
2. Prefer dependency injection
# Good: Inject dependency
class WeatherService:
def __init__(self, api_client):
self.api = api_client
# Test
def test_weather_service():
fake_api = FakeWeatherAPI()
service = WeatherService(fake_api)
# Less good: Patch global dependency
@patch('weather_service.requests')
def test_weather_service(mock_requests):
pass
3. Keep mocks simple
# Good: Simple, focused mock
mock_db = Mock()
mock_db.find_user.return_value = {"name": "Alice"}
# Bad: Overly complex mock
mock_db = Mock()
mock_db.connect.return_value = True
mock_db.cursor.return_value.execute.return_value = None
mock_db.cursor.return_value.fetchone.return_value = ("Alice",)
4. Test behavior, not implementation
# Good: Test what the code does
def test_sends_notification():
mock_sender = Mock()
service = NotificationService(mock_sender)
service.notify_user(123, "Hello")
mock_sender.send.assert_called()
# Bad: Test how the code works
def test_notification_implementation():
mock_sender = Mock()
service = NotificationService(mock_sender)
service.notify_user(123, "Hello")
# Too specific about internal details
assert service._prepare_message_called
assert service._format_recipient_called
Complete example: Order service
# test_order_service.py
from unittest.mock import Mock, patch
import pytest
from order_service import OrderService
class TestOrderService:
def test_place_order_success(self):
mock_inventory = Mock()
mock_inventory.check_availability.return_value = True
mock_inventory.reserve.return_value = True
mock_payment = Mock()
mock_payment.charge.return_value = {"status": "success"}
service = OrderService(
inventory=mock_inventory,
payment=mock_payment
)
order = service.place_order(
item_id="ABC123",
quantity=2,
card_token="tok_visa"
)
assert order["status"] == "confirmed"
mock_inventory.reserve.assert_called_once_with("ABC123", 2)
mock_payment.charge.assert_called_once()
def test_place_order_out_of_stock(self):
mock_inventory = Mock()
mock_inventory.check_availability.return_value = False
mock_payment = Mock()
service = OrderService(
inventory=mock_inventory,
payment=mock_payment
)
with pytest.raises(Exception, match="Out of stock"):
service.place_order("ABC123", 2, "tok_visa")
# Payment should never be called
mock_payment.charge.assert_not_called()
Wrapping up
We've covered:
- Mock - Create mock objects with configurable behavior
- MagicMock - Mock with magic method support
- patch - Temporarily replace objects during tests
- pytest-mock - Cleaner mocking with pytest
- side_effect - Configure exceptions or multiple return values
- Assertions - Verify how mocks were called
Key takeaways
- Mock external dependencies, not internal code
- Prefer dependency injection over patching
- Keep mocks simple and focused
- Test behavior, not implementation details
- Use mocking to test error conditions
Mocking is a powerful tool, but use it wisely. The goal is testable, maintainable code - not maximum mock coverage!