Python Mocking and Monkeypatching — Core Concepts

Why this topic matters

Modern Python code depends on APIs, queues, filesystems, and clocks. Tests that hit all those systems directly become slow and flaky. Mocking and monkeypatching provide control.

Mocking vs monkeypatching

  • Mocking: replace an object with a programmable test double that records calls.
  • Monkeypatching: temporarily swap attributes/functions/variables at runtime.

In unittest, mocking is usually done with unittest.mock.patch. In pytest, monkeypatching is often done with the monkeypatch fixture.

Practical examples

Mock an external email client:

from unittest.mock import patch

@patch("app.notifications.send_email")
def test_welcome_email_sent(mock_send):
    register_user("dev@example.com")
    mock_send.assert_called_once()

Monkeypatch an environment variable:

def test_uses_sandbox(monkeypatch):
    monkeypatch.setenv("PAYMENTS_MODE", "sandbox")
    assert payment_mode() == "sandbox"

Where to patch (critical rule)

Patch where the symbol is used, not where defined. If module service.py imports send_email, patch service.send_email, not notifications.send_email.

This single rule prevents many “mock did nothing” debugging sessions.

Common misconception

“More mocks means better tests.” Usually false. Excessive mocks couple tests to implementation details and break refactors. Prefer behavior-focused tests and mock only external boundaries.

Healthy testing boundaries

Good candidates for mocking:

  • network clients
  • payment gateways
  • cloud SDK calls
  • random/time providers

Poor candidates for mocking:

  • pure domain logic
  • simple helper functions
  • objects with no external side effects

Design feedback loop

If mocking is painful, architecture may be too tangled. Dependency injection, smaller functions, and clear interfaces reduce mocking complexity.

A useful smell test: if writing a test needs six patches before any assertion, the production code likely needs decomposition.

Related topics: Python Unittest Framework and Python Debugging with PDB.

The one thing to remember: mock the boundaries, keep the core real.

Choosing between stubs, fakes, and mocks

Not every dependency needs a mock object. A simple in-memory fake can be easier to reason about and closer to real behavior.

  • use stubs for fixed responses
  • use fakes for lightweight realistic behavior
  • use mocks for interaction verification

Picking the lightest tool that solves the problem keeps tests readable.

Keeping trust in test outcomes

If a test passes only because every dependency is mocked, add at least one integration check for that path. This balances speed with realism.

Adoption playbook

A practical way to roll out mocking and monkeypatching is to start with one critical workflow, set a measurable success signal, and review results after two weeks. Keep the first rollout intentionally small so the team learns the tool and failure modes without creating delivery risk. After the pilot is stable, document the standards in your engineering handbook and automate checks in CI. Small, repeated improvements usually beat dramatic one-time migrations.

pythontestingtooling

See Also