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.
See Also
- Python Acceptance Testing Patterns How Python teams verify software does what real users actually asked for.
- Python Approval Testing How approval testing lets you verify complex Python output by comparing it to a saved 'golden' copy you already checked.
- Python Behavior Driven Development Get an intuitive feel for Behavior Driven Development so Python behavior stops feeling unpredictable.
- Python Browser Automation Testing How Python can control a web browser like a robot to test websites automatically.
- Python Chaos Testing Applications Why breaking your own Python systems on purpose makes them stronger.