Mocking with monkeypatch and unittest.mock

pytest · Practical · 12 min

Tests must not call real payment APIs or wait on networks, and mocks let you test the logic around them.

Mocking replaces a dependency with a stand-in you control. pytest works with the standard library's unittest.mock:

  • Mock() / MagicMock() accept any call or attribute. Set return_value or side_effect (an exception to raise, or a function or list of values).
  • After the call, assert on usage: m.assert_called_once_with(...), m.call_count, m.call_args.
  • create_autospec(func) or Mock(spec=Cls) restrict the mock to the real signature, so calls with wrong arguments fail instead of silently passing.

To install a mock, either use monkeypatch.setattr(module, "name", fake) or unittest.mock.patch("pkg.module.name") as a context manager. The pytest-mock plugin wraps the latter as a mocker fixture.

The key rule: patch where the name is looked up, not where it's defined. If shop/checkout.py does from shop.payments import charge, patch shop.checkout.charge; patching shop.payments.charge leaves checkout's own reference untouched.

Mock at boundaries (HTTP, clock, filesystem, third-party SDKs), not your own internal functions. Over-mocking produces tests that pass while the real integration is broken, and that break on every refactor. For HTTP specifically, libraries like responses or respx fake the transport more realistically.

Example

# test_checkout.py
from unittest.mock import Mock
class PaymentError(Exception): pass

def checkout(cart_total, gateway):
    try:
        return gateway.charge(amount=cart_total, currency="EUR")["id"]
    except PaymentError:
        return None

def test_checkout_charges_total():
    gateway = Mock()
    gateway.charge.return_value = {"id": "ch_1"}
    assert checkout(42, gateway) == "ch_1"
    gateway.charge.assert_called_once_with(amount=42, currency="EUR")

def test_checkout_handles_decline():
    gateway = Mock()
    gateway.charge.side_effect = PaymentError("declined")
    assert checkout(42, gateway) is None

def test_patch_module_attr(monkeypatch):
    import time
    monkeypatch.setattr(time, "time", lambda: 1_700_000_000.0)
    assert time.time() == 1_700_000_000.0

Remember

  • Mock with return_value / side_effect, then assert calls
  • Use spec/autospec so mocks enforce real signatures
  • Patch the name where it is looked up, not where it's defined
  • Mock at system boundaries, not your own internals

Watch out for

  • Patching pkg.payments.charge while the code under test imported it with from-import into pkg.checkout.

Check yourself

  1. checkout.py does `from payments import charge`. Which target should patch() use?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.pytest.org/en/stable/how-to/monkeypatch.html

Builds on

Unlocks

Nothing builds on this yet.