Why Mocking?
Problems Without Mocking
# Code to test
def get_user_name(user_id):
response = requests.get(f'https://api.example.com/users/{user_id}')
return response.json()['name']
# Problems:
# 1. Makes real HTTP calls (slow)
# 2. Depends on external service (unreliable)
# 3. Costs money (API calls)
# 4. Tests non-deterministic (network issues)
# 5. Cannot test error scenarios
Solutions with Mocking
from unittest.mock import Mock, patch
# Mock the external dependency
@patch('requests.get')
def test_get_user_name(mock_get):
# Configure mock
mock_get.return_value.json.return_value = {'name': 'Alice'}
# Test with mock
result = get_user_name(123)
assert result == 'Alice'
# Verify interactions
mock_get.assert_called_once_with('https://api.example.com/users/123')
Types of Test Doubles
| Type | Description | Use Case |
|---|---|---|
| Dummy | Passed around, never used | Fill parameters |
| Stub | Returns predetermined values | Provide test data |
| Mock | Verifies interactions | Check method calls |
| Spy | Records calls, passes through | Observe real behavior |
| Fake | Working implementation | Simplified database |
Mock vs Stub
# Stub: Returns fixed value
def stub_get_user(user_id):
return {'id': user_id, 'name': 'Test User'}
# Mock: Verifies interactions
mock = Mock()
mock.get_user(123)
assert mock.get_user.called
assert mock.get_user.call_args == ((123,),)
unittest.mock Basics
Mock Objects
from unittest.mock import Mock, MagicMock
# Create mock
mock_obj = Mock()
# Configure return values
mock_obj.method.return_value = 42
print(mock_obj.method()) # 42
# Configure side effects (exceptions)
mock_obj.method.side_effect = ValueError('Bad input')
mock_obj.method() # Raises ValueError
# Configure side effects (multiple values)
mock_obj.method.side_effect = [1, 2, 3]
print(mock_obj.method()) # 1
print(mock_obj.method()) # 2
print(mock_obj.method()) # 3
Call Assertions
mock = Mock()
# Call the mock
mock('arg1', 'arg2')
mock('arg3', key='value')
# Assertions
assert mock.called
assert mock.call_count == 2
assert mock.call_args == (('arg3',), {'key': 'value'})
assert mock.call_args_list == [
(('arg1', 'arg2'), {}),
(('arg3',), {'key': 'value'})
]
# Reset mock
mock.reset_mock()
assert mock.call_count == 0
patch() Decorator
from unittest.mock import patch
# Patch where the object is used
@patch('module.function')
def test_something(mock_func):
mock_func.return_value = 'mocked'
result = function_using_func()
assert result == 'mocked'
# Multiple patches
@patch('module.func1')
@patch('module.func2')
def test_multiple(mock2, mock1): # Reversed order!
mock1.return_value = 1
mock2.return_value = 2
assert function_using_both() == 3
# Context manager
with patch('module.function') as mock:
mock.return_value = 'mocked'
result = function_using_func()
assert result == 'mocked'
MagicMock vs Mock
from unittest.mock import Mock, MagicMock
# Mock: Basic mock
mock = Mock()
mock.__len__.return_value = 5
len(mock) # 5
# MagicMock: Supports magic methods
magic = MagicMock()
len(magic) # 0 (default)
repr(magic) # '<MagicMock id='...'>'
magic + 1 # MagicMock()
'hello' in magic # True
Mocking Patterns
Mocking Database
from unittest.mock import Mock, patch
# Code to test
def create_user(db, name, email):
if db.user_exists(email):
raise ValueError('User already exists')
user = db.insert_user(name, email)
return user
# Test
def test_create_user():
mock_db = Mock()
mock_db.user_exists.return_value = False
mock_db.insert_user.return_value = {'id': 1, 'name': 'Alice'}
result = create_user(mock_db, 'Alice', 'alice@example.com')
assert result == {'id': 1, 'name': 'Alice'}
mock_db.user_exists.assert_called_once_with('alice@example.com')
mock_db.insert_user.assert_called_once_with('Alice', 'alice@example.com')
# Test error case
def test_create_user_exists():
mock_db = Mock()
mock_db.user_exists.return_value = True
with pytest.raises(ValueError) as exc_info:
create_user(mock_db, 'Alice', 'alice@example.com')
assert 'already exists' in str(exc_info.value)
Mocking File I/O
from unittest.mock import mock_open, patch
# Code to test
def read_config(filename):
with open(filename) as f:
return f.read()
# Test
def test_read_config():
mock_data = '{"key": "value"}'
with patch('builtins.open', mock_open(read_data=mock_data)) as mock_file:
result = read_config('config.json')
assert result == mock_data
mock_file.assert_called_once_with('config.json')
Mocking HTTP Requests
import requests
from unittest.mock import patch, Mock
def fetch_user(user_id):
response = requests.get(f'https://api.example.com/users/{user_id}')
response.raise_for_status()
return response.json()
# Test success
def test_fetch_user():
with patch('requests.get') as mock_get:
mock_response = Mock()
mock_response.json.return_value = {'id': 1, 'name': 'Alice'}
mock_response.raise_for_status.return_value = None
mock_get.return_value = mock_response
result = fetch_user(1)
assert result == {'id': 1, 'name': 'Alice'}
# Test error
def test_fetch_user_not_found():
with patch('requests.get') as mock_get:
mock_response = Mock()
mock_response.raise_for_status.side_effect = requests.HTTPError('404')
mock_get.return_value = mock_response
with pytest.raises(requests.HTTPError):
fetch_user(999)
Best Practices
# ✅ Do:
# 1. Mock at the boundary (external services, I/O)
# 2. Use Mock/MagicMock for complex objects
# 3. Assert on interactions, not implementation
# 4. Keep tests simple and focused
# 5. Clean up mocks
# ❌ Don't:
# 1. Mock too much - test behavior, not implementation
# 2. Mock built-in functions unless necessary
# 3. Create overly complex mock setups
# 4. Forget to assert on mocks
# 5. Use mocks for simple value testing
Alternative: pytest-mock
# pytest-mock provides mocker fixture
def test_with_mocker(mocker):
# mocker is a wrapper around unittest.mock
mock_func = mocker.patch('module.function')
mock_func.return_value = 'mocked'
result = function_using_func()
assert result == 'mocked'