Mocking in pytest

· 6 min read · Updated March 13, 2026 · intermediate
pytest testing mocking unittest test-doubles tdd

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/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:

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

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 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 function
  • utils.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