Riddikulus! Defeating the Boggart of Dependencies


pytest Deep-Dives — Hogwarts Edition

👻 Why Mock? — The Boggart Problem

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)

🐒 monkeypatch.setattr — Simple Spell Replacement

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.

🎭 Fake Objects — Manual Test Doubles (Decoy Detonators)

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"]

🃏 unittest.mock.Mock & MagicMock — Shape-Shifting Boggarts Under Control

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's mocker Fixture — The Patronus of Mocking

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")

⚔️ mock.patch vs monkeypatch — Duel of the Patchers

Featuremonkeypatchmock.patch / mocker
CleanupAutomatic (fixture)Automatic (fixture/context mgr)
Call tracking❌ Not built-in✅ assert_called_once_with, call_count
Return valuesManual lambda/functionreturn_value, side_effect
Best forEnv vars, simple attrsComplex interactions, verifying calls
DependencyBuilt into pytestpytest-mock (external)

✅ Asserting Calls — Verifying the Spell Was Cast Correctly

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)