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-mockIf you are managing dependencies with a requirements file:
pip install pytest-mock
pip freeze | grep pytest-mock >> requirements-dev.txtpytest-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 # expectedIn 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.utilsRight — 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 == 2Using 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_responseMocking 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 NoneYou 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.