Mocking Pytest: A Practical Guide to Test Doubles in Python
Every test suite eventually hits a wall: real APIs are slow, databases need setup, and email servers are not reachable from CI. Mocking pytest solves this problem by letting you swap real dependencies for controlled doubles that return exactly what your test needs. The Python standard library ships unittest.mock for this, and the pytest-mock plugin wraps it in a fixture-friendly API.
This guide covers the practical patterns: installing the plugin, patching at the right import location, configuring return values and side effects, asserting on calls, and avoiding the common traps that make mock-heavy tests brittle. See the pytest basics tutorial for context if you are new to pytest, and the working with files guide when you need to mock filesystem code.
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. The mocker fixture handles mock creation and cleanup automatically, you never need to call start() or stop() manually as you would with raw unittest.mock.patch. After installation it’s wise to verify the plugin is registered correctly with your pytest installation, because a missing plugin can waste hours of debugging when mock assertions silently pass without actually intercepting anything. The version output below confirms pytest-mock is active and ready.
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. Before you can mock anything, you need to understand the real dependency you are replacing. The send_welcome_email function below represents the kind of external side effect that makes integration testing impractical, it connects to an SMTP server, which requires network access, valid credentials, and a recipient mailbox. By mocking this function during unit tests, you can verify that your application code calls it with the right arguments without ever touching a mail server.
# 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. The challenge is that register_user imports and calls this function directly, so you need to intercept that call path without modifying the production code. The function below represents a typical registration flow, it performs some domain logic and then sends a welcome email as a side effect. In a real codebase this function would likely also validate the email address, check for duplicate usernames, and persist data, but we keep it minimal here to focus on the mocking technique.
# 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}
Now the test below uses mocker.patch to replace send_welcome_email at the point where the app module imports it. The mock intercepts every call, records the arguments it received, and returns whatever value you configure, all without touching an SMTP server. This follows the Arrange-Act-Assert pattern: configure the mock, invoke the function under test, then verify both the return value and the mock’s call history. The assertion assert_called_once_with confirms the exact argument the email function received.
# 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. This tripwire catches developers repeatedly because the instinct is to target the source module, but Python resolves imports at the usage site, so the patch must intercept the name binding in the module that actually calls the function.
Understanding mock objects
The MagicMock is the default mock type that pytest-mock creates. It responds to any attribute access or method call without complaint, which makes tests quick to write but also means a typo in a method name won’t raise an error, the mock just returns another mock. You can inspect the call history, pre-configure return values, and set side effects to gain precise control. The interactive example below shows the essential operations: creating a mock, calling arbitrary methods, and pinning a specific response with return_value.
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 than the decorator approach because it integrates directly with pytest’s fixture system, no extra imports needed. The fixture also handles cleanup automatically after each test function returns, resetting every patch it created so they never leak state between tests. This avoids the subtle bugs that happen when a @patch decorator is accidentally left on a test that no longer needs the mock, or when a manual patcher.stop() call gets skipped during an early assertion failure. For most projects the fixture approach is the better default.
def test_with_mocker_fixture(mocker):
mock = mocker.patch("app.send_welcome_email")
mock.return_value = True
# Test code
pass
The mocker fixture resets every patch it created once the test function returns, so you never have to call stop() or stopall() yourself. This automatic cleanup is one of the biggest quality-of-life improvements over raw unittest.mock.patch, where a forgotten stop call can cause maddening test-order-dependent failures that only appear in CI.
Mocking classes
Mocking a class replaces the class itself so that any code that instantiates it gets a mock instance instead. This is useful for dependencies that communicate over the network, read from disk, or perform expensive computation, you substitute a lightweight double that you fully control. The class definition below is a stand-in for a real API client you might use in production.
# api_client.py
class APIClient:
def get_user(self, user_id):
# Makes HTTP request
return {"id": user_id, "name": "Real User"}
The test below patches the class import so that create_and_fetch_user receives a mock instance with a pre-configured return value for get_user(), letting you verify the orchestration logic without making a network round-trip. The chained .return_value.get_user.return_value syntax drills through mock nesting levels: the first .return_value configures the mock APIClient instance, and .get_user.return_value sets what that method returns.
# 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 rather than replacing the entire class. The spec parameter constrains your mock so that it only accepts attribute accesses and method calls that exist on the real class, preventing silent errors when a test references a method that was renamed or removed during a refactor. This is a lightweight alternative to autospec that works well when you only need signature checking on a single object.
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
The simplest mock configuration is setting a fixed return value. Every call to the patched function returns the same value, which is fine for functions called once in a test but can hide bugs when your code calls the mock multiple times and expects different results. The example below shows the basic pattern.
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
When your code calls a mock repeatedly and should receive different values each time, use side_effect with a list. The mock returns elements from that list in order, consuming one per call. When the list is exhausted, calling the mock again either raises StopIteration or returns the last value depending on your setup. This pattern is essential for testing loops, retry logic, or paginated API responses where each call should yield a distinct result.
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
When you need the mock’s response to depend on its input arguments, rather than returning a pre-computed list, assign a callable to side_effect. The mock invokes your function with whatever arguments it received and returns the result. This lets you implement conditional logic inside the mock, which is particularly helpful when testing code that branches on the return value of a dependency.
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
The third major use of side_effect is raising exceptions on demand to simulate failure conditions. This is how you verify your error-handling code works correctly, patch the dependency, set side_effect to the exception type, and assert that your code responds as expected. Testing error paths is one of the strongest arguments for mocking, because triggering genuine failures in external services during tests is unreliable at best and impossible at worst.
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. Unlike a mock which completely replaces the dependency, a spy wraps the real function and delegates to it by default, capturing call information along the way. This is useful when you need to verify that a side effect happened but still want the original behavior to execute, for example, confirming that a logging call was made without suppressing the actual log output.
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 pytest module-level functions
Mocking functions from imported modules works the same way as mocking instance methods, but you need to pay attention to how Python resolves names at the import site. The utility function below reads a JSON config file from disk, a common pattern that creates a hard dependency on the filesystem. Mocking both open and json.load lets you test the config loading logic with any data you choose, without creating temporary files.
# utils.py
import os
import json
def load_config():
with open("config.json") as f:
return json.load(f)
The test patches two targets: builtins.open (a builtin, which must be patched by its fully-qualified name) and utils.json (the module reference inside the utils namespace). By mocking json.load to return a controlled dictionary, you can verify that load_config reads and parses correctly without touching the filesystem at all.
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
The single most common mistake with mocking is patching the wrong module. Since Python caches module imports in sys.modules, you must patch the reference in the module where the function is called, not where it’s defined. The first snippet below would silently fail, the mock would replace notifications.send_welcome_email but app.send_welcome_email still points to the original. The second snippet targets the correct location.
# Wrong — patching where defined
@patch("notifications.send_welcome_email")
# Correct — patching where used
@patch("app.send_welcome_email")
Forgetting to assert
Mocks pass silently by default, a test that creates a mock but never asserts on it will succeed even if the function under test does absolutely nothing. Every mock you create should have at least one assertion verifying it was called with the expected arguments. The bad example below is dangerously misleading because it looks like a meaningful test but exercises zero verification. The good example calls assert_called_once() to confirm the mock actually intercepted a call.
# 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 part of Python itself and require patching with their fully qualified module path. The example below mocks datetime.datetime so that datetime.now() returns a fixed timestamp, useful for testing time-dependent code without sleeping or relying on wall-clock time. The key detail is that you patch datetime.datetime, not datetime, because the class lives inside the module.
# 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
When your code uses a with statement, mocking the context manager requires configuring two levels of the mock object: the return value of the __enter__ method, which represents what the as clause binds. The pattern below mocks builtins.open so that read() on the file handle returns a controlled string, simulating a successful file read without any file on disk.
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
Autospec creates a mock that mirrors the signature of a real object, refusing calls with the wrong argument names or counts. This catches refactoring mistakes, if you rename a parameter in the real function but forget to update the mock’s call site, autospec raises an error instead of silently succeeding. Use it for critical dependencies where argument correctness matters.
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
A good rule of thumb for mocking: intercept at the architectural boundary where your code meets the outside world. Mocking an HTTP client or database adapter isolates your logic from infrastructure concerns, while mocking an internal helper function couples your test to implementation details that may change during a refactor. The two snippets below illustrate the difference.
# Good — mock the external API
@patch("app.requests.get")
# Avoid — mock something deep inside your code
@patch("app.internal.module.SomeClass")
Keep mocks simple
Over-configuring a mock with every possible attribute makes tests hard to read and maintain. Set only the properties your test actually depends on, the mock will return sensible defaults for everything else. The first example below is harder to scan and more brittle; the second is all you actually need.
# 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
Give your mocks descriptive names that reflect what they replace, not generic variable names. In a test with multiple mocks, names like mock1 and m make it impossible to tell which dependency failed an assertion. The clear naming below documents intent at a glance.
# 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
When a test fails, you want the error message to tell you whether the mock was never called or whether it returned the wrong value. Calling assert_called_once() before checking the return value separates these two failure modes and gives you a clearer diagnosis.
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 pytest dependencies 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.
See also
- pytest-basics, Getting started with pytest framework
- error-handling, Handling exceptions in Python
- unittest-module, Python’s built-in testing framework