Python Mocking with unittest.mock: Complete Guide
Master Python's unittest.mock library for testing. Learn Mock, MagicMock, patch, side_effect, and spec with real-world examples for unit testing.
What you'll learn
- ✓Use Mock and MagicMock to replace dependencies
- ✓Patch objects with @patch and context managers
- ✓Control return values and side effects
- ✓Verify how mocked objects were called
Prerequisites
- •Python functions and classes
- •Basic understanding of unit testing
Why Mock?
Unit tests should test one thing in isolation. But real code has dependencies: databases, APIs, file systems, the current time, random numbers. Mocking replaces those dependencies with controlled stand-ins so your tests are fast, deterministic, and focused.
Python’s unittest.mock module (part of the standard library since Python 3.3) provides Mock, MagicMock, and patch for exactly this purpose.
Mock and MagicMock Basics
A Mock object accepts any attribute access or method call without raising an error. It records everything that happens to it, so you can assert on it later.
from unittest.mock import Mock, MagicMock
# Create a mock
mock = Mock()
# Call it like a function
mock(1, 2, key='value')
# Access attributes
mock.some_attribute
mock.nested.deep.attribute
# Everything returns another Mock
result = mock.some_method(42)
print(type(result)) # <class 'unittest.mock.Mock'>
# Check how it was called
mock.assert_called_once_with(1, 2, key='value')
mock.some_method.assert_called_with(42)
MagicMock vs Mock
MagicMock is a Mock subclass that pre-implements magic methods (__len__, __iter__, __getitem__, etc.):
from unittest.mock import Mock, MagicMock
# Regular Mock: magic methods raise TypeError
regular = Mock()
# len(regular) # TypeError: object of type 'Mock' has no len()
# MagicMock: magic methods work
magic = MagicMock()
len(magic) # Returns 0 (default)
magic[0] # Returns a Mock
iter(magic) # Returns an iterator
bool(magic) # Returns True
# Configure magic method return values
magic.__len__.return_value = 5
print(len(magic)) # 5
magic.__getitem__.return_value = 'item'
print(magic[0]) # 'item'
Use MagicMock by default. Use Mock only when you explicitly want magic methods to fail.
Setting Return Values
from unittest.mock import Mock
# Simple return value
mock_db = Mock()
mock_db.get_user.return_value = {'id': 1, 'name': 'Alice', 'email': 'alice@example.com'}
user = mock_db.get_user(1)
print(user['name']) # 'Alice'
# Nested return values
mock_api = Mock()
mock_api.client.get.return_value.status_code = 200
mock_api.client.get.return_value.json.return_value = {'data': [1, 2, 3]}
response = mock_api.client.get('/api/items')
print(response.status_code) # 200
print(response.json()) # {'data': [1, 2, 3]}
Side Effects
side_effect lets you control what happens when a mock is called. It can raise exceptions, return different values on successive calls, or run a custom function.
from unittest.mock import Mock
# Raise an exception
mock_db = Mock()
mock_db.connect.side_effect = ConnectionError("Database is down")
# mock_db.connect() # Raises ConnectionError
# Return different values on successive calls
mock_input = Mock()
mock_input.side_effect = ['Alice', '30', 'alice@example.com']
print(mock_input()) # 'Alice'
print(mock_input()) # '30'
print(mock_input()) # 'alice@example.com'
# Custom function as side effect
def fake_lookup(user_id):
users = {1: 'Alice', 2: 'Bob'}
if user_id not in users:
raise KeyError(f"User {user_id} not found")
return users[user_id]
mock_service = Mock()
mock_service.get_user.side_effect = fake_lookup
print(mock_service.get_user(1)) # 'Alice'
print(mock_service.get_user(2)) # 'Bob'
# mock_service.get_user(99) # Raises KeyError
Assertions: Verifying Calls
After exercising the code under test, you verify the mock was used correctly:
from unittest.mock import Mock, call
mock = Mock()
# Make some calls
mock.process('a')
mock.process('b')
mock.process('c')
# Was it called at all?
mock.process.assert_called()
# Was it called exactly 3 times?
assert mock.process.call_count == 3
# What was the last call?
mock.process.assert_called_with('c')
# Check all calls in order
mock.process.assert_has_calls([
call('a'),
call('b'),
call('c'),
])
# Check calls regardless of order
mock.process.assert_has_calls([
call('c'),
call('a'),
], any_order=True)
# Was it NEVER called with a specific argument?
mock.save = Mock()
mock.save.assert_not_called()
Accessing Call Arguments
from unittest.mock import Mock
mock = Mock()
mock.send_email('alice@test.com', subject='Hello', body='Hi there')
# Get the arguments from the last call
args, kwargs = mock.send_email.call_args
print(args) # ('alice@test.com',)
print(kwargs) # {'subject': 'Hello', 'body': 'Hi there'}
# Get all calls
mock.send_email('bob@test.com', subject='Hey')
for c in mock.send_email.call_args_list:
print(c)
Patching with @patch
patch temporarily replaces an object with a mock during a test. This is the most important tool for isolating code.
The Key Rule: Patch Where It Is Used
You patch the name as it is imported in the module under test, not where it is defined:
# myapp/email_service.py
import smtplib
def send_welcome_email(user_email):
server = smtplib.SMTP('smtp.example.com', 587)
server.starttls()
server.login('app@example.com', 'password')
server.sendmail('app@example.com', user_email, 'Welcome!')
server.quit()
return True
# tests/test_email_service.py
from unittest.mock import patch, MagicMock
from myapp.email_service import send_welcome_email
# Patch smtplib.SMTP in the module where it's USED
@patch('myapp.email_service.smtplib.SMTP')
def test_send_welcome_email(mock_smtp_class):
# Configure the mock
mock_server = MagicMock()
mock_smtp_class.return_value = mock_server
# Call the function
result = send_welcome_email('user@test.com')
# Verify
assert result is True
mock_smtp_class.assert_called_once_with('smtp.example.com', 587)
mock_server.starttls.assert_called_once()
mock_server.sendmail.assert_called_once_with(
'app@example.com', 'user@test.com', 'Welcome!'
)
mock_server.quit.assert_called_once()
Patching as a Context Manager
from unittest.mock import patch
def test_with_context_manager():
with patch('myapp.service.requests.get') as mock_get:
mock_get.return_value.status_code = 200
mock_get.return_value.json.return_value = {'users': []}
# Code under test
from myapp.service import fetch_users
result = fetch_users()
assert result == []
mock_get.assert_called_once()
Patching Multiple Objects
from unittest.mock import patch
@patch('myapp.service.cache.get')
@patch('myapp.service.db.query')
@patch('myapp.service.logger')
def test_get_user(mock_logger, mock_query, mock_cache):
# Note: decorators are applied bottom-up, so arguments
# are in reverse order!
mock_cache.return_value = None # Cache miss
mock_query.return_value = {'id': 1, 'name': 'Alice'}
from myapp.service import get_user
user = get_user(1)
assert user['name'] == 'Alice'
mock_cache.assert_called_once_with('user:1')
mock_query.assert_called_once_with('SELECT * FROM users WHERE id = ?', (1,))
mock_logger.info.assert_called()
patch.object
Patch a specific attribute on an object:
from unittest.mock import patch
class PaymentGateway:
def charge(self, amount, card_token):
# In real code, this calls an external API
pass
def refund(self, transaction_id):
pass
def test_payment():
gateway = PaymentGateway()
with patch.object(gateway, 'charge', return_value={'status': 'success', 'id': 'txn_123'}):
result = gateway.charge(99.99, 'tok_visa')
assert result['status'] == 'success'
patch.dict
Temporarily modify a dictionary (commonly used for environment variables):
from unittest.mock import patch
import os
@patch.dict(os.environ, {'API_KEY': 'test-key-123', 'DEBUG': 'true'})
def test_with_env_vars():
assert os.environ['API_KEY'] == 'test-key-123'
assert os.environ['DEBUG'] == 'true'
# After the test, os.environ is restored to its original state
@patch.dict(os.environ, {'SECRET': ''}, clear=True)
def test_with_clean_env():
# os.environ only has 'SECRET', everything else is cleared
assert 'HOME' not in os.environ
Using spec for Safer Mocks
By default, mocks accept any attribute, which can hide bugs. spec restricts the mock to only allow attributes that exist on the real object:
from unittest.mock import Mock, create_autospec
class UserRepository:
def get(self, user_id: int) -> dict:
pass
def save(self, user: dict) -> None:
pass
def delete(self, user_id: int) -> bool:
pass
# Without spec: typos go unnoticed
bad_mock = Mock()
bad_mock.gett(1) # No error! But 'gett' is a typo
# With spec: typos raise AttributeError
safe_mock = Mock(spec=UserRepository)
# safe_mock.gett(1) # AttributeError: Mock object has no attribute 'gett'
safe_mock.get(1) # Works fine
# create_autospec also checks argument signatures
auto_mock = create_autospec(UserRepository)
auto_mock.get(1) # Works
# auto_mock.get(1, 2, 3) # TypeError: too many positional arguments
Always use spec or create_autospec in production test suites. It catches refactoring bugs where a method is renamed but tests still pass because the mock silently accepts the old name.
Real-World Testing Patterns
Testing API Clients
from unittest.mock import patch, Mock
import requests
class WeatherClient:
def __init__(self, api_key):
self.api_key = api_key
self.base_url = 'https://api.weather.com'
def get_temperature(self, city):
response = requests.get(
f'{self.base_url}/current',
params={'city': city, 'key': self.api_key}
)
response.raise_for_status()
data = response.json()
return data['temperature']
@patch('requests.get')
def test_get_temperature(mock_get):
# Set up the mock response
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {
'city': 'London',
'temperature': 18.5,
'unit': 'celsius'
}
mock_response.raise_for_status = Mock()
mock_get.return_value = mock_response
# Test
client = WeatherClient('fake-key')
temp = client.get_temperature('London')
assert temp == 18.5
mock_get.assert_called_once_with(
'https://api.weather.com/current',
params={'city': 'London', 'key': 'fake-key'}
)
@patch('requests.get')
def test_get_temperature_api_error(mock_get):
mock_response = Mock()
mock_response.raise_for_status.side_effect = requests.HTTPError("503 Server Error")
mock_get.return_value = mock_response
client = WeatherClient('fake-key')
try:
client.get_temperature('London')
assert False, "Should have raised"
except requests.HTTPError:
pass # Expected
Testing Time-Dependent Code
from unittest.mock import patch
from datetime import datetime
def get_greeting():
hour = datetime.now().hour
if hour < 12:
return "Good morning"
elif hour < 18:
return "Good afternoon"
else:
return "Good evening"
@patch('__main__.datetime')
def test_morning_greeting(mock_datetime):
mock_datetime.now.return_value = datetime(2026, 7, 6, 9, 0, 0)
assert get_greeting() == "Good morning"
@patch('__main__.datetime')
def test_evening_greeting(mock_datetime):
mock_datetime.now.return_value = datetime(2026, 7, 6, 20, 0, 0)
assert get_greeting() == "Good evening"
Testing File Operations
from unittest.mock import patch, mock_open
def read_config(path):
with open(path) as f:
lines = f.readlines()
config = {}
for line in lines:
line = line.strip()
if '=' in line and not line.startswith('#'):
key, value = line.split('=', 1)
config[key.strip()] = value.strip()
return config
def test_read_config():
fake_file_content = """
# Database config
DB_HOST=localhost
DB_PORT=5432
DB_NAME=myapp
"""
with patch('builtins.open', mock_open(read_data=fake_file_content)):
config = read_config('/etc/myapp/config')
assert config['DB_HOST'] == 'localhost'
assert config['DB_PORT'] == '5432'
assert config['DB_NAME'] == 'myapp'
assert '#' not in str(config.keys()) # Comments are excluded
Common Mistakes
Patching the Wrong Location
# myapp/utils.py
from os.path import exists
def check_file(path):
return exists(path)
# WRONG: patching where it's defined
@patch('os.path.exists') # This won't work because utils.py already imported it
def test_wrong(mock_exists):
pass
# RIGHT: patching where it's used
@patch('myapp.utils.exists') # Patch the name in the module that imported it
def test_right(mock_exists):
mock_exists.return_value = True
from myapp.utils import check_file
assert check_file('/some/path') is True
Over-Mocking
# BAD: mocking everything makes the test meaningless
def test_over_mocked():
with patch('myapp.calculate') as mock_calc:
mock_calc.return_value = 42
assert mock_calc(1, 2) == 42 # You're testing the mock, not your code!
# GOOD: only mock external dependencies
def test_properly_mocked():
with patch('myapp.service.external_api.fetch') as mock_fetch:
mock_fetch.return_value = {'rate': 1.5}
result = calculate_with_live_rate(100)
assert result == 150.0 # Testing YOUR calculation logic
Wrapping Up
Mocking is essential for writing fast, reliable unit tests. Start with patch to replace external dependencies (APIs, databases, file I/O), use return_value and side_effect to control behavior, and use assertions like assert_called_once_with to verify interactions. Always use spec or create_autospec to catch attribute errors early. The most common mistake is patching the wrong location — remember to patch where an object is used, not where it is defined.
Related articles
- Python Python Testing with pytest: Fixtures, Marks, and Plugins
Master pytest from fixtures and parametrize to marks, plugins, and CI integration. Write fast, maintainable Python tests with practical examples.
- Python Dependency Injection in Python Without a Framework
Learn how to apply dependency injection in Python using constructor injection, factory functions, and simple containers -- no framework required.
- Python Advanced pytest Patterns for Real-World Projects
Master pytest fixtures, parametrize, monkeypatch, and custom markers to write maintainable, expressive tests for production Python code.
- FastAPI Testing FastAPI Apps with TestClient and Pytest Fixtures
Build a robust test suite for FastAPI with TestClient, pytest fixtures, dependency overrides, and async testing patterns.