Patching the Right Thing with mocker
Short answer:
mocker is a pytest fixture provided by the pytest-mock plugin.
It provides a small helper object (usually called mocker) that wraps Python’s standard library unittest.mock so you can:
- Replace (“patch”) real objects (functions, methods, attributes) with controllable
MagicMockfakes. - Track calls: check how many times a fake ran, which arguments it saw, and related details.
- Automatically undo all patches at the end of each test so your code returns to normal.
1. Code Example
email_gateway.py
import smtplib
from email.message import EmailMessage
def send_email(recipient: str, subject: str, body: str) -> str:
"""Deliver transactional email through our SMTP relay."""
# deliver a message via SMTP
message = EmailMessage()
message["From"] = "noreply@example.com"
message["To"] = recipient
message["Subject"] = subject
message.set_content(body)
try:
with smtplib.SMTP("mail.example.com", 587, timeout=3) as client:
client.starttls()
client.login("noreply@example.com", "super-secret")
client.send_message(message)
except OSError:
# Swallow transport errors for this illustrative example.
pass
return f"sent:{recipient}:{subject}"
email_service.py
from email_gateway import send_email
def notify_user(recipient: str, message: str) -> str:
"""Send the standard account-update email copy to a user."""
subject = "Account update"
body = f"Hello!\n\n{message}"
return send_email(recipient, subject, body)
Because the high-level notify_user function imports send_email and closes over that name, tests patch email_service.send_email, instead of email_gateway.send_email.
2. What should be the target?
Rule of thumb:
Patch the name visible inside the module under test, not the original place where the object was first defined. In other words, patch “where it is used” instead of “where it is defined”.
3. A minimal custom PocketMocker
Now let’s build a mini version of mocker to show all the moving parts.
We won’t touch unittest.mock.patch; instead we’ll do the work manually with:
importlib.import_modulegetattr/setattrunittest.mock.MagicMock
pocket_mocker.py
import importlib
from unittest.mock import MagicMock
class PocketMocker:
"""Tiny helper to mimic the patching API readers see from pytest-mock."""
def __init__(self):
self._stack = []
def patch(self, target: str, replacement=None, **kwargs):
module_path, attribute = target.rsplit(".", 1)
module = importlib.import_module(module_path)
original = getattr(module, attribute)
if replacement is None:
replacement = MagicMock(**kwargs)
setattr(module, attribute, replacement)
self._stack.append((module, attribute, original))
return replacement
def stopall(self):
while self._stack:
module, attribute, original = self._stack.pop()
setattr(module, attribute, original)
The low-level helpers explained
-
importlib.import_module(module_path)- Takes a string such as
"email_service.send_email"and returns the actual module object. - Relies on Python’s import machinery and
sys.modules.
- Takes a string such as
-
getattr(module, attr_name)- Reads an attribute on an object.
- Equivalent to
module.attr_name.
-
setattr(module, attr_name, replacement)- Sets an attribute on an object.
- Equivalent to
module.attr_name = replacement. - This is the literal “patch” step: we overwrite the original function with our fake.
-
MagicMock(**kwargs)-
Creates a flexible fake object that:
- Records calls (so you can assert later).
- Can be configured with
return_valueorside_effect. - Supports attributes and methods dynamically.
-
Building PocketMocker shows that pytest-mock plus unittest.mock are simply a nicer layer over this same core behavior.
Tests
test_email_service.py
import email_service
from pocket_mocker import PocketMocker
def test_notify_user_with_pytest_mocker(mocker):
mocker.patch(
"email_service.send_email", return_value="sent:me@example.com:Account update"
)
result = email_service.notify_user("me@example.com", "You have a new badge!")
assert result == "sent:me@example.com:Account update"
def test_notify_user_with_pocket_mocker():
pocket = PocketMocker()
pocket.patch(
"email_service.send_email", return_value="sent:you@example.com:Account update"
)
result = email_service.notify_user("you@example.com", "Password changed")
assert result == "sent:you@example.com:Account update"
pocket.stopall()
Test output
test_email_service.py ..
============================= 2 passed in 0.05s =============================
The first test exercises the real pytest-mock fixture, while the second walks through the DIY implementation so you can trace every step.
Research Notes
Expand to read the full research notes, references, and comparisons gathered for this topic.