Python unittest.mock: Mocking Objects and Functions
Unit tests should test units in isolation. When your function calls an external API, queries a database, or reads from the filesystem, you're no longer testing your code—you're testing the entire...
Key Insights
- Mock where the object is used, not where it’s defined—this is the most common source of “why isn’t my mock working?” frustration
- Use
MagicMockby default unless you have a specific reason to useMock; it handles Python’s magic methods automatically - Always prefer
autospec=Truewhen patching to catch signature mismatches that would otherwise silently pass your tests
Introduction to Mocking
Unit tests should test units in isolation. When your function calls an external API, queries a database, or reads from the filesystem, you’re no longer testing your code—you’re testing the entire system. Mocking solves this by replacing real objects with controlled substitutes that behave exactly how you specify.
Python’s unittest.mock module (part of the standard library since Python 3.3) provides everything you need. It’s powerful, flexible, and occasionally confusing. This article covers the patterns you’ll actually use in production code.
The core principle is simple: replace the thing you don’t control with something you do control. Your tests become faster, more reliable, and truly isolated.
The Mock and MagicMock Classes
The Mock class is the foundation of unittest.mock. It creates objects that record how they’re used and let you specify their behavior.
from unittest.mock import Mock, MagicMock
# Basic Mock usage
api_client = Mock()
api_client.get_user.return_value = {"id": 1, "name": "Alice"}
# Use the mock
result = api_client.get_user(user_id=1)
print(result) # {"id": 1, "name": "Alice"}
# Verify the call
api_client.get_user.assert_called_once_with(user_id=1)
MagicMock extends Mock with default implementations of magic methods like __len__, __iter__, and __getitem__. Use MagicMock unless you specifically need to test that magic methods aren’t called.
# Mock doesn't handle magic methods by default
regular_mock = Mock()
# len(regular_mock) # TypeError!
# MagicMock does
magic_mock = MagicMock()
magic_mock.__len__.return_value = 5
print(len(magic_mock)) # 5
# Iteration works too
magic_mock.__iter__.return_value = iter([1, 2, 3])
print(list(magic_mock)) # [1, 2, 3]
Key attributes you’ll use constantly:
return_value: What the mock returns when calledside_effect: Exceptions to raise, values to return sequentially, or a function to callcall_args: The arguments from the most recent callcall_count: How many times the mock was called
Using the @patch Decorator
The @patch decorator temporarily replaces an object during a test. The critical concept is patch where the object is used, not where it’s defined.
# services/user_service.py
from external_api import APIClient
def get_user_profile(user_id):
client = APIClient()
user_data = client.fetch_user(user_id)
return {
"display_name": user_data["name"].upper(),
"email": user_data["email"]
}
# tests/test_user_service.py
from unittest.mock import patch, MagicMock
from services.user_service import get_user_profile
class TestGetUserProfile:
# Patch where APIClient is USED (services.user_service)
# NOT where it's defined (external_api)
@patch('services.user_service.APIClient')
def test_returns_formatted_profile(self, mock_api_class):
# Configure the mock instance
mock_client = MagicMock()
mock_client.fetch_user.return_value = {
"name": "alice",
"email": "alice@example.com"
}
mock_api_class.return_value = mock_client
result = get_user_profile(123)
assert result == {
"display_name": "ALICE",
"email": "alice@example.com"
}
mock_client.fetch_user.assert_called_once_with(123)
The context manager form is useful when you only need to patch part of a test:
def test_handles_api_failure(self):
# Setup phase without mocking
user_cache = UserCache()
# Only mock during the specific operation
with patch('services.user_service.APIClient') as mock_api_class:
mock_client = MagicMock()
mock_client.fetch_user.side_effect = ConnectionError("API down")
mock_api_class.return_value = mock_client
result = get_user_profile(123)
assert result is None # Or however you handle failures
Patching Object Attributes and Methods
When you need to mock a specific method on an existing class rather than the whole class, use patch.object():
# database/repository.py
class UserRepository:
def __init__(self, connection):
self.connection = connection
def find_by_id(self, user_id):
cursor = self.connection.execute(
"SELECT * FROM users WHERE id = ?", (user_id,)
)
return cursor.fetchone()
def save(self, user):
self.connection.execute(
"INSERT INTO users (name, email) VALUES (?, ?)",
(user["name"], user["email"])
)
self.connection.commit()
# tests/test_repository.py
from unittest.mock import patch, MagicMock
from database.repository import UserRepository
class TestUserRepository:
def test_find_by_id_returns_user(self):
mock_connection = MagicMock()
mock_cursor = MagicMock()
mock_cursor.fetchone.return_value = (1, "Alice", "alice@example.com")
mock_connection.execute.return_value = mock_cursor
repo = UserRepository(mock_connection)
result = repo.find_by_id(1)
assert result == (1, "Alice", "alice@example.com")
mock_connection.execute.assert_called_once_with(
"SELECT * FROM users WHERE id = ?", (1,)
)
@patch.object(UserRepository, 'find_by_id')
def test_service_uses_repository(self, mock_find):
mock_find.return_value = {"id": 1, "name": "Alice"}
repo = UserRepository(None) # Connection doesn't matter
user = repo.find_by_id(1)
assert user["name"] == "Alice"
For multiple patches, stack the decorators (note the reversed order of arguments):
@patch('module.ClassC')
@patch('module.ClassB')
@patch('module.ClassA')
def test_multiple_patches(self, mock_a, mock_b, mock_c):
# mock_a corresponds to ClassA (innermost decorator)
# mock_c corresponds to ClassC (outermost decorator)
pass
Controlling Mock Behavior with side_effect
The side_effect attribute is your tool for complex mock behavior. It handles three scenarios:
Raising exceptions:
from unittest.mock import Mock
import pytest
def test_handles_connection_error():
http_client = Mock()
http_client.get.side_effect = ConnectionError("Network unreachable")
with pytest.raises(ConnectionError):
http_client.get("https://api.example.com/users")
Returning different values on successive calls:
def test_retry_logic():
http_client = Mock()
# First two calls fail, third succeeds
http_client.get.side_effect = [
ConnectionError("Timeout"),
ConnectionError("Timeout"),
{"status": 200, "data": "success"}
]
# Your retry logic should handle this
result = fetch_with_retry(http_client, "/api/data", max_retries=3)
assert result["status"] == 200
assert http_client.get.call_count == 3
Using a custom function:
def test_dynamic_responses():
def mock_fetch(url):
if "users" in url:
return {"type": "user", "count": 10}
elif "posts" in url:
return {"type": "post", "count": 50}
raise ValueError(f"Unknown endpoint: {url}")
api = Mock()
api.fetch.side_effect = mock_fetch
assert api.fetch("/api/users")["type"] == "user"
assert api.fetch("/api/posts")["count"] == 50
Assertions and Call Inspection
Mocks record every interaction. Use this for precise verification:
from unittest.mock import Mock, call, ANY
def test_logging_calls():
logger = Mock()
# Simulate code that logs
logger.info("Starting process")
logger.debug("Processing item", extra={"item_id": 42})
logger.info("Process complete")
# Verify specific calls
logger.info.assert_any_call("Starting process")
# Verify call count
assert logger.info.call_count == 2
assert logger.debug.call_count == 1
# Verify exact call sequence
logger.info.assert_has_calls([
call("Starting process"),
call("Process complete")
])
# Use ANY for flexible matching
logger.debug.assert_called_with("Processing item", extra=ANY)
Inspect call arguments directly when assertions aren’t flexible enough:
def test_call_inspection():
notifier = Mock()
notifier.send("user@example.com", subject="Hello", priority=1)
notifier.send("admin@example.com", subject="Alert", priority=5)
# Get all calls
all_calls = notifier.send.call_args_list
# Find high-priority calls
high_priority = [
c for c in all_calls
if c.kwargs.get("priority", 0) > 3
]
assert len(high_priority) == 1
assert high_priority[0].args[0] == "admin@example.com"
Best Practices and Common Pitfalls
Use autospec for safer mocks. Without it, mocks accept any arguments, hiding bugs:
# The real class
class EmailService:
def send(self, to, subject, body):
pass
# Without autospec - this passes but is WRONG
def test_without_autospec():
mock_service = Mock()
mock_service.send("wrong", "args") # No error!
mock_service.send.assert_called() # Passes
# With autospec - catches the error
from unittest.mock import create_autospec
def test_with_autospec():
mock_service = create_autospec(EmailService)
# mock_service.send("wrong", "args") # TypeError: missing required argument 'body'
mock_service.send("to@example.com", "Subject", "Body") # Correct
Or use autospec=True with @patch:
@patch('services.email.EmailService', autospec=True)
def test_email_sent(self, mock_service_class):
mock_instance = mock_service_class.return_value
# Now mock_instance.send() enforces the real signature
Don’t over-mock. If you’re mocking more than you’re testing, reconsider your design. Mocks should isolate external dependencies, not replace the code you’re trying to test.
Reset mocks when reusing them:
def test_multiple_scenarios():
api = Mock()
api.call("first")
assert api.call.call_count == 1
api.reset_mock() # Clear all call history
api.call("second")
assert api.call.call_count == 1 # Count reset
Prefer dependency injection over patching. If your code accepts dependencies as parameters, you can pass mocks directly without patching:
# Better: accepts dependency
def process_order(order, payment_gateway):
payment_gateway.charge(order.total)
# Test is simpler
def test_process_order():
mock_gateway = Mock()
order = Order(total=100)
process_order(order, mock_gateway)
mock_gateway.charge.assert_called_once_with(100)
Mocking is a tool, not a goal. Use it to isolate your code from things you can’t control, verify interactions with dependencies, and test error handling. But always remember: the best test is one that exercises real code paths with minimal mocking.