How to Mock in pytest: pytest-mock Guide

How to Mock in pytest: pytest-mock Guide

Unit tests should be fast, repeatable, and independent. The moment your test reaches out to a database, a third-party API, or the filesystem, those properties are at risk. Mocking solves this by replacing real dependencies with controlled stand-ins that behave exactly as your test needs them to.

pytest-mock is the standard way to mock in pytest. It wraps Python's built-in unittest.mock library and exposes everything through a single mocker fixture — keeping your tests clean and your teardown automatic.

Why Mocking Matters in Unit Tests

Consider a function that sends a welcome email after a user registers. In a unit test you want to verify the logic around that call — was the right address used? was the template correct? — without actually sending an email. Without mocking, you either hit a real mail server (slow, side-effectful, potentially expensive) or you restructure your entire codebase to make the dependency injectable.

Mocking lets you intercept the call at the point where it leaves your code, replace it with a fake, and then assert against what happened. The result is a test that runs in milliseconds, never fails due to network conditions, and tells you exactly what your code did.

Installing pytest-mock

pip install pytest-mock

If you are managing dependencies with a requirements file:

pip install pytest-mock
pip freeze | grep pytest-mock >> requirements-dev.txt

pytest-mock requires pytest 6.0 or later and works with Python 3.8+. Once installed, the mocker fixture is available automatically in every test — no imports needed.

The mocker Fixture — Basic Usage

The mocker fixture is injected by pytest-mock and lives for the duration of a single test. Any patches applied through it are automatically reversed when the test ends, so there is no cleanup code to write.

def test_basic_mock(mocker):
    mock_fn = mocker.MagicMock(return_value=42)
    result = mock_fn("hello")
    assert result == 42
    mock_fn.assert_called_once_with("hello")

This creates a standalone mock, calls it, and verifies the call. The mocker object gives you access to MagicMock, Mock, patch, patch.object, spy, and everything else from unittest.mock.

mocker.patch() — Patching Objects and Functions

mocker.patch() replaces a named object for the duration of the test. The first argument is a dotted path string — the module and name where the object is used, not where it is defined.

# myapp/notifications.py
import smtplib

def send_welcome_email(address):
    with smtplib.SMTP("smtp.example.com") as server:
        server.sendmail("no-reply@example.com", address, "Welcome!")
    return True
# tests/test_notifications.py
def test_send_welcome_email(mocker):
    mock_smtp = mocker.patch("myapp.notifications.smtplib.SMTP")
    mock_instance = mock_smtp.return_value.__enter__.return_value

    from myapp.notifications import send_welcome_email
    result = send_welcome_email("user@example.com")

    assert result is True
    mock_instance.sendmail.assert_called_once_with(
        "no-reply@example.com", "user@example.com", "Welcome!"
    )

The patch lasts exactly as long as the test. When the test finishes, smtplib.SMTP is restored to its original value.

mocker.patch.object() — Patching Class Methods

When you have a reference to the actual class or object (rather than a string path), mocker.patch.object() is cleaner and more refactor-safe.

# myapp/payment.py
class PaymentGateway:
    def charge(self, amount, card_token):
        # real API call
        ...

class OrderService:
    def __init__(self, gateway: PaymentGateway):
        self.gateway = gateway

    def place_order(self, amount, card_token):
        result = self.gateway.charge(amount, card_token)
        return result
# tests/test_order.py
from myapp.payment import PaymentGateway, OrderService

def test_place_order_calls_gateway(mocker):
    gateway = PaymentGateway()
    mocker.patch.object(gateway, "charge", return_value={"status": "ok"})

    service = OrderService(gateway)
    result = service.place_order(99.99, "tok_test")

    assert result == {"status": "ok"}
    gateway.charge.assert_called_once_with(99.99, "tok_test")

patch.object takes the actual object and the attribute name as a string. This is safer than string-based patching because your IDE can catch renames.

MagicMock vs Mock

Python's mock library ships two main classes: Mock and MagicMock.

Mock is a blank slate. Every attribute access and call returns another Mock. It does not implement any dunder (magic) methods.

MagicMock is a Mock that also pre-configures magic methods — __len__, __iter__, __enter__, __exit__, __str__, and so on. This means you can use a MagicMock anywhere Python expects a context manager or an iterable without extra setup.

def test_mock_vs_magicmock(mocker):
    plain = mocker.Mock()
    magic = mocker.MagicMock()

    # MagicMock supports len()
    magic.__len__.return_value = 5
    assert len(magic) == 5

    # Plain Mock raises TypeError for len()
    try:
        len(plain)
    except TypeError:
        pass  # expected

In practice, default to MagicMock (which is what mocker.patch creates by default) unless you specifically want to detect accidental use of magic methods.

Verifying Calls: assert_called_once_with, call_count, call_args

Once a mock has been called, it records everything — arguments, keyword arguments, the number of times it was called. pytest-mock exposes the full unittest.mock API for asserting on this history.

def test_call_verification(mocker):
    mock_save = mocker.patch("myapp.db.save_record")

    from myapp.db import process_batch
    process_batch([{"id": 1}, {"id": 2}])

    # Was it called at all?
    mock_save.assert_called()

    # Was it called exactly once?
    # mock_save.assert_called_once()

    # Called twice — check total count
    assert mock_save.call_count == 2

    # Check specific arguments on the last call
    mock_save.assert_called_with({"id": 2})

    # Inspect every call
    first_call_args = mock_save.call_args_list[0]
    assert first_call_args == mocker.call({"id": 1})

The full list of assertion helpers: assert_called(), assert_called_once(), assert_called_with(*args, **kwargs), assert_called_once_with(*args, **kwargs), assert_any_call(*args, **kwargs), assert_not_called().

Patching Context: Where to Patch

The most common mocking mistake is patching the wrong location. Python's import system means that once a name is imported into a module, that module holds its own reference to it. You must patch the name in the module where it is used, not where it was originally defined.

# myapp/utils.py
from datetime import datetime

def get_today_label():
    return datetime.now().strftime("%Y-%m-%d")

Wrong — patching the source:

mocker.patch("datetime.datetime.now")  # does not affect myapp.utils

Right — patching the name in the module that uses it:

def test_get_today_label(mocker):
    mock_now = mocker.patch("myapp.utils.datetime")
    mock_now.now.return_value.strftime.return_value = "2026-01-01"

    from myapp.utils import get_today_label
    assert get_today_label() == "2026-01-01"

The rule: patch it where it's imported, not where it lives.

Side Effects and Return Values

return_value sets what a mock returns on every call. side_effect gives you finer control — it can be a list of values (returned one per call), a function (called with the same arguments), or an exception class.

def test_retry_on_failure(mocker):
    mock_fetch = mocker.patch("myapp.client.fetch_data")

    # First call raises, second call succeeds
    mock_fetch.side_effect = [ConnectionError("timeout"), {"data": [1, 2, 3]}]

    from myapp.client import fetch_with_retry
    result = fetch_with_retry("https://api.example.com/items")

    assert result == {"data": [1, 2, 3]}
    assert mock_fetch.call_count == 2

Using a callable as side_effect lets you inspect arguments and return different values dynamically:

def dynamic_response(url):
    if "users" in url:
        return [{"id": 1}]
    return []

mock_fetch.side_effect = dynamic_response

Mocking Exceptions

Testing error-handling paths is just as important as testing the happy path. Set side_effect to an exception class or instance to make the mock raise it.

def test_handles_api_error(mocker):
    mocker.patch(
        "myapp.client.requests.get",
        side_effect=requests.exceptions.Timeout("Request timed out"),
    )

    from myapp.client import get_user_profile
    result = get_user_profile(user_id=42)

    # Expect the function to return a fallback, not propagate the exception
    assert result is None

You can also use side_effect to raise on specific calls while succeeding on others, which is useful for testing retry logic and circuit breakers.

def test_circuit_breaker(mocker):
    mock_call = mocker.patch("myapp.service.external_call")
    mock_call.side_effect = [
        ValueError("bad data"),
        ValueError("bad data"),
        ValueError("bad data"),
    ]

    from myapp.service import call_with_circuit_breaker
    with pytest.raises(RuntimeError, match="Circuit open"):
        call_with_circuit_breaker()

End-to-End Testing Without Mocks

Mocking isolates units. For tests that verify real user workflows in a browser, HelpMeTest runs end-to-end tests with AI-generated scenarios and 24/7 monitoring — no mocking required.

Start testing free →

Read more

ScyllaDB Testing Guide: Cassandra Driver Compatibility, Shard-per-Core Testing & Performance Regression

ScyllaDB Testing Guide: Cassandra Driver Compatibility, Shard-per-Core Testing & Performance Regression

ScyllaDB delivers Cassandra-compatible APIs with a rewritten Seastar-based engine that achieves dramatically higher throughput. Testing ScyllaDB applications requires validating both Cassandra compatibility and ScyllaDB-specific behaviors like shard-per-core data distribution. This guide covers both angles. ScyllaDB Testing Landscape ScyllaDB is a drop-in replacement for Cassandra at the API level—which means

By HelpMeTest