pyguides

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/method
  • MagicMock, 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

FeatureMockSpy
Replaces real objectYesNo (wraps real object)
Records callsYesYes
Can call real implementationNoYes (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 function
  • utils.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