A Boggart is a shape-shifting creature that takes the form of your worst fear. External dependencies (APIs, databases, owl-post services) are the Boggarts of testing: unpredictable, slow, and terrifying.
Mocking transforms the Boggart into something harmless — Riddikulus! — letting you test YOUR code in isolation.
# The "real" dependency — an owl post service that's slow and unreliable
class OwlPostService:
def send_letter(self, recipient: str, content: str) -> bool:
"""Sends an owl across Britain. Takes hours. Might fail in storms."""
# ... actual network call to the Owl Post Office ...
raise NotImplementedError("Real owls needed!")
# Our code that USES the dependency
class HogwartsNotifier:
def __init__(self, post_service: OwlPostService):
self.post_service = post_service
def notify_acceptance(self, student_name: str) -> bool:
letter = f"Dear {student_name}, you have been accepted to Hogwarts!"
return self.post_service.send_letter(student_name, letter)
def test_notify_with_monkeypatch(monkeypatch):
"""Replace the method directly — like Polyjuice for functions."""
service = OwlPostService()
# Replace the scary method with a tame one
monkeypatch.setattr(service, "send_letter", lambda recipient, content: True)
notifier = HogwartsNotifier(post_service=service)
result = notifier.notify_acceptance("Harry Potter")
assert result is True
Simple and clean, but you can't easily verify how the method was called.
class FakeOwlPostService:
"""A hand-crafted fake — like a Decoy Detonator from Weasleys'."""
def __init__(self):
self.letters_sent = []
def send_letter(self, recipient: str, content: str) -> bool:
self.letters_sent.append({"to": recipient, "content": content})
return True
def test_notify_with_fake():
fake_service = FakeOwlPostService()
notifier = HogwartsNotifier(post_service=fake_service)
result = notifier.notify_acceptance("Hermione Granger")
assert result is True
assert len(fake_service.letters_sent) == 1
assert "Hermione" in fake_service.letters_sent[0]["content"]
from unittest.mock import Mock, MagicMock
def test_notify_with_mock():
"""Mock objects auto-create attributes and track calls."""
mock_service = Mock(spec=OwlPostService)
mock_service.send_letter.return_value = True
notifier = HogwartsNotifier(post_service=mock_service)
result = notifier.notify_acceptance("Ron Weasley")
assert result is True
# Verify HOW the mock was called — the Boggart reveals itself!
mock_service.send_letter.assert_called_once_with(
"Ron Weasley",
"Dear Ron Weasley, you have been accepted to Hogwarts!"
)
def test_magic_mock_dunder_methods():
"""MagicMock supports __len__, __iter__, etc. — full shapeshifter."""
mock_spellbook = MagicMock()
mock_spellbook.__len__.return_value = 42
mock_spellbook.__getitem__.return_value = "Expelliarmus"
assert len(mock_spellbook) == 42
assert mock_spellbook[0] == "Expelliarmus"
pytest-mock wraps unittest.mock in a fixture called mocker that automatically cleans up patches after each test — no manual stop needed.
# pip install pytest-mock
def test_notify_with_mocker(mocker):
"""mocker.patch replaces at the MODULE level — like Obliviate on an import."""
# Patch where it's USED, not where it's defined
mock_send = mocker.patch.object(OwlPostService, "send_letter", return_value=True)
service = OwlPostService()
notifier = HogwartsNotifier(post_service=service)
result = notifier.notify_acceptance("Neville Longbottom")
assert result is True
mock_send.assert_called_once()
# Check the content argument
call_args = mock_send.call_args
assert "Neville Longbottom" in call_args[1].get("content", call_args[0][1])
def test_spy_on_real_method(mocker):
"""mocker.spy wraps the real method — observes without replacing."""
# Like an Invisibility Cloak: watch without interfering
spy = mocker.spy(HogwartsNotifier, "notify_acceptance")
fake_service = Mock(spec=OwlPostService)
fake_service.send_letter.return_value = True
notifier = HogwartsNotifier(post_service=fake_service)
notifier.notify_acceptance("Luna Lovegood")
spy.assert_called_once_with(notifier, "Luna Lovegood")
| Feature | monkeypatch | mock.patch / mocker |
|---|---|---|
| Cleanup | Automatic (fixture) | Automatic (fixture/context mgr) |
| Call tracking | ❌ Not built-in | ✅ assert_called_once_with, call_count |
| Return values | Manual lambda/function | return_value, side_effect |
| Best for | Env vars, simple attrs | Complex interactions, verifying calls |
| Dependency | Built into pytest | pytest-mock (external) |
from unittest.mock import Mock, call
def test_multiple_notifications():
"""Verify a sequence of calls — like checking the Marauder's Map trail."""
mock_service = Mock(spec=OwlPostService)
mock_service.send_letter.return_value = True
notifier = HogwartsNotifier(post_service=mock_service)
notifier.notify_acceptance("Harry")
notifier.notify_acceptance("Ron")
notifier.notify_acceptance("Hermione")
# Total calls
assert mock_service.send_letter.call_count == 3
# Verify specific call order
mock_service.send_letter.assert_any_call("Hermione",
"Dear Hermione, you have been accepted to Hogwarts!")
# Verify ALL calls in order
expected_calls = [
call("Harry", "Dear Harry, you have been accepted to Hogwarts!"),
call("Ron", "Dear Ron, you have been accepted to Hogwarts!"),
call("Hermione", "Dear Hermione, you have been accepted to Hogwarts!"),
]
mock_service.send_letter.assert_has_calls(expected_calls, any_order=False)