Mocking in pytest
When you write tests, you often need to isolate the code under test from its dependencies. Maybe your function calls an external API, reads from a database, or sends an email. Testing these scenarios directly would be slow, unreliable, or impossible in a test environment.
This is where mocking comes in. A mock is a fake object that mimics the behavior of a real dependency. You can control what the mock returns, verify how it was called, and simulate edge cases—all without touching the real external service.
This guide covers mocking in pytest using unittest.mock and the pytest-mock plugin.
Installing pytest-mock
First, install the pytest-mock plugin:
pip install pytest-mock
This plugin adds a mocker fixture that provides a convenient interface to mock objects.
Verify it’s installed:
pytest --version -q
# output: pytest 8.x.x
# output: pytest-mock: enabled
Your First Mock
Suppose you have a function that sends emails:
# notifications.py
def send_welcome_email(user_email):
# Imagine this connects to an SMTP server
print(f"Sending welcome email to {user_email}")
return True
You want to test a function that calls send_welcome_email without actually sending emails:
# app.py
from notifications import send_welcome_email
def register_user(username, email):
# Some registration logic
send_welcome_email(email)
return {"username": username, "email": email}
Here’s how to mock the email function:
# test_app.py
def test_register_user_sends_email(mocker):
# Arrange: mock the send_welcome_email function
mock_email = mocker.patch("app.send_welcome_email")
mock_email.return_value = True
# Act: call the function under test
result = register_user("alice", "alice@example.com")
# Assert: the result is correct
assert result == {"username": "alice", "email": "alice@example.com"}
# Verify the mock was called
mock_email.assert_called_once_with("alice@example.com")
Notice the mock is patched on app.send_welcome_email, not notifications.send_welcome_email. You patch where the object is used, not where it’s defined.
Understanding Mock Objects
The MagicMock is the default mock type. It responds to any attribute access or method call:
from unittest.mock import MagicMock
mock = MagicMock()
# Any attribute or method returns another MagicMock
result = mock.anything()
print(result) # <MagicMock name='mock.anything()' return_value>
# Set a return value
mock.method.return_value = 42
print(mock.method()) # 42
Mock vs MagicMock
Mock— returns another Mock for any attribute/methodMagicMock— like Mock but also supports Python magic methods (__str__,__len__, etc.)
Use MagicMock for most cases. Use Mock when you need to verify no magic method was called.
Patching Strategies
patch() as a Decorator
For multiple tests, use the decorator style:
from unittest.mock import patch
@patch("app.send_welcome_email")
def test_register_user_sends_email(mock_email):
mock_email.return_value = True
result = register_user("bob", "bob@example.com")
assert result["username"] == "bob"
mock_email.assert_called_once()
The mocker Fixture
The mocker fixture from pytest-mock is cleaner:
def test_with_mocker_fixture(mocker):
mock = mocker.patch("app.send_welcome_email")
mock.return_value = True
# Test code
pass
The mocker fixture automatically handles cleanup after each test.
Mocking Classes
Mocking a class creates instances of that class:
# api_client.py
class APIClient:
def get_user(self, user_id):
# Makes HTTP request
return {"id": user_id, "name": "Real User"}
# test_api_client.py
def test_get_user_with_mock(mocker):
# Mock the entire class
mock_client = mocker.patch("app.APIClient")
# Configure what the mock instance returns
mock_client.return_value.get_user.return_value = {"id": 1, "name": "Test User"}
# Now when code creates an APIClient, it gets our mock
result = create_and_fetch_user(1) # Uses APIClient internally
assert result["name"] == "Test User"
Mocking Instance Methods
Sometimes you want to mock a specific method on a real object. Use spec:
from unittest.mock import Mock, spec
def test_with_spec(mocker):
# spec limits what the mock can do
mock_obj = mocker.Mock(spec=SomeRealClass)
# Only real attributes/methods work
mock_obj.real_method() # Works
mock_obj.fake_method() # Raises AttributeError
Return Values and Side Effects
Setting Return Values
def test_return_value(mocker):
mock = mocker.patch("app.some_function")
mock.return_value = "expected result"
result = some_function()
assert result == "expected result"
Returning Different Values Per Call
Use side_effect to return different values on successive calls:
def test_multiple_return_values(mocker):
mock = mocker.patch("app.fetch_next_item")
mock.side_effect = [{"item": "first"}, {"item": "second"}, StopIteration]
assert fetch_next_item() == {"item": "first"}
assert fetch_next_item() == {"item": "second"}
fetch_next_item() # Raises StopIteration
Side Effect Functions
You can also pass a function as side_effect:
def test_side_effect_function(mocker):
mock = mocker.patch("app.process_data")
def process(value):
return value * 2
mock.side_effect = process
assert process_data(5) == 10
assert process_data(3) == 6
Raising Exceptions
Simulate errors by raising exceptions:
def test_raises_exception(mocker):
mock = mocker.patch("app.save_to_database")
mock.side_effect = ConnectionError("Database unavailable")
with pytest.raises(ConnectionError, match="Database unavailable"):
save_to_database({"key": "value"})
Spies
A spy records how it was called while optionally forwarding to the real implementation. Use spies to verify behavior without replacing it entirely:
def test_with_spy(mocker):
# Create a spy on the real function
spy = mocker.spy(app, "send_welcome_email")
register_user("charlie", "charlie@example.com")
# Verify it was called with the right arguments
spy.assert_called_once_with("charlie@example.com")
Spy vs Mock
| Feature | Mock | Spy |
|---|---|---|
| Replaces real object | Yes | No (wraps real object) |
| Records calls | Yes | Yes |
| Can call real implementation | No | Yes (by default) |
Use spies when you want to verify a method was called but still want the real behavior.
Mocking Module-Level Functions
Mocking functions from imported modules works the same way:
# utils.py
import os
import json
def load_config():
with open("config.json") as f:
return json.load(f)
def test_load_config_with_mocks(mocker):
# Mock the built-in open function
mock_open = mocker.patch("builtins.open")
# Mock json.load to return test data
mock_json = mocker.patch("utils.json")
mock_json.load.return_value = {"debug": True}
# Test the function
config = load_config()
assert config["debug"] is True
Notice the patch targets:
builtins.open— the built-in open functionutils.json— the json module inside utils
Common Pitfalls
Wrong Patch Location
# Wrong — patching where defined
@patch("notifications.send_welcome_email")
# Correct — patching where used
@patch("app.send_welcome_email")
Forgetting to Assert
Mocks pass by default. Always assert what you expect:
# Bad — test always passes
def test_something(mocker):
mocker.patch("app.function")
do_something()
# Good — actually verifies behavior
def test_something(mocker):
mock = mocker.patch("app.function")
do_something()
mock.assert_called_once()
Mocking Built-ins
Some functions are built into Python and require specific patching:
# To mock datetime.now()
from datetime import datetime
from unittest.mock import patch
@patch("datetime.datetime")
def test_time(mock_datetime):
mock_datetime.now.return_value = datetime(2025, 1, 1)
# Now your code sees the mocked time
Advanced: Mocking Context Managers
Mock objects as context managers:
def test_with_context_manager(mocker):
mock_file = mocker.patch("builtins.open")
mock_file.return_value.__enter__.return_value.read.return_value = "file contents"
content = read_file("anything.txt")
assert content == "file contents"
Advanced: Autospec
Use autospec to automatically match the signature of the real object:
from unittest.mock import create_autospec
def test_autospec(mocker):
real_func = some_complex_function
mock = mocker.create_autospec(real_func)
# mock has the same signature as real_func
mock(arg1, arg2=5) # Type checking still works
This prevents mocks from accepting wrong arguments by accident.
Best Practices
Mock at the Right Boundary
Mock external dependencies, not internal implementation details:
# Good — mock the external API
@patch("app.requests.get")
# Avoid — mock something deep inside your code
@patch("app.internal.module.SomeClass")
Keep Mocks Simple
Don’t over-configure mocks:
# Unnecessary complexity
mock = mocker.patch("app.function")
mock.return_value = True
mock.called = True
mock.call_count = 1
# Just what you need
mock = mocker.patch("app.function")
mock.return_value = True
Name Mocks Clearly
Use descriptive names:
# Good
mock_api_client = mocker.patch("app.APIClient")
mock_send_email = mocker.patch("app.send_email")
# Confusing
mock1 = mocker.patch("app.APIClient")
m = mocker.patch("app.send_email")
Verify, Then Assert
Check the mock was called before asserting return values:
def test_order(mocker):
mock = mocker.patch("app.function")
mock.return_value = 42
result = do_something()
mock.assert_called_once() # Verify it was called
assert result == 42 # Then check result
Conclusion
Mocking is essential for writing fast, reliable tests. Replace slow or external dependencies with controlled mocks, verify your code calls them correctly, and simulate edge cases that would be difficult to trigger in reality.
Start with simple mocks using the mocker fixture, move to spies when you need partial real behavior, and use autospec for type-safe mocking. The pytest-mock plugin integrates smoothly with pytest and makes your tests cleaner.
See Also
- pytest-basics — Getting started with pytest framework
- error-handling — Handling exceptions in Python
- unittest-module — Python’s built-in testing framework