Tests should be fast, deterministic, and isolated. A function that hits a real HTTP API, or reads a file, or queries a database, is none of those at test time — slow, flaky, full of side effects. Mocking lets you replace those external dependencies with stand-ins that return what you tell them to. The function being tested doesn't know the difference.
unittest.mock — Mock and patch
Mock() creates an object that records every call and accepts any attribute access. patch("module.path") replaces something in another module's namespace for the duration of the test (with-block, decorator, or context manager). The combination handles 90% of mocking needs.
The patch path — module-where-it's-used, not where-it's-defined
The most common mocking bug: patch("requests.get") when the code being tested does from requests import get; get(...). After import, the function in the test target's namespace is a separate reference; patching the original module doesn't affect it. The fix: patch("my_module.get") — patch where it's looked up, not where it's defined.
monkeypatch — pytest's lighter alternative
pytest's monkeypatch fixture provides similar capabilities with auto-cleanup at test end. monkeypatch.setattr(module, "attr", value) swaps something. monkeypatch.setenv("VAR", "value") sets an env var. Common pattern: the cleaner mock for pytest tests.
War Story: Over-mocking is a real anti-pattern. Tests that mock everything end up testing the mocks instead of the code. Pippa's stack mocks at boundaries — the network, the LLM API, the time function — but not internal calls between modules. The line: mock across processes and trust the code within.
Code
Mock — basics·python
from unittest.mock import Mock
# Create a mock — it accepts any call, returns a Mock
m = Mock()
m.do_something("foo", count=5)
m.other.deep.call()
# Inspect what happened
print(m.do_something.called) # True
print(m.do_something.call_args) # call('foo', count=5)
print(m.do_something.call_count) # 1
# Set return values
m.compute.return_value = 42
print(m.compute()) # 42
print(m.compute("any", "args")) # 42 — args ignored
# Side effects — for raising or sequenced returns
m.flaky.side_effect = [1, 2, ValueError("oops")]
print(m.flaky()) # 1
print(m.flaky()) # 2
try:
m.flaky()
except ValueError:
print("raised on third")
patch — replacing in another module·python
# my_module.py
# import requests
# def fetch_user(uid):
# resp = requests.get(f"https://api.example.com/{uid}")
# return resp.json()
# test_my_module.py
from unittest.mock import patch, Mock
def test_fetch_user():
fake_resp = Mock()
fake_resp.json.return_value = {"name": "alice", "id": 42}
# Patch where it's USED — my_module.requests, not just requests
with patch("my_module.requests.get", return_value=fake_resp) as fake_get:
# from my_module import fetch_user
# result = fetch_user(42)
# assert result == {"name": "alice", "id": 42}
# fake_get.assert_called_once_with("https://api.example.com/42")
pass
patch as decorator — cleaner for whole tests·python
from unittest.mock import patch, Mock
@patch("my_module.requests.get")
def test_with_decorator(mock_get):
mock_get.return_value.json.return_value = {"x": 1}
# ... call my_module.fetch ...
# ... assert mock_get.called ...
# Multiple patches stack — innermost first in the parameter list
@patch("my_module.B")
@patch("my_module.A")
def test_stacked(mock_A, mock_B):
# mock_A is the inner @patch, listed first
# mock_B is the outer @patch, listed second
pass
pytest's monkeypatch fixture·python
# Cleaner alternative to patch when you're already in pytest
def test_with_monkeypatch(monkeypatch):
# Replace an attribute
import my_module
fake_get = lambda url: {"fake": True}
monkeypatch.setattr(my_module, "fetch", fake_get)
# ... test code ...
# auto-cleaned at test end
# Set env var (auto-restored)
monkeypatch.setenv("DEBUG", "true")
# ... test code ...
# Change directory (auto-restored)
monkeypatch.chdir("/tmp")
Asserting on calls·python
from unittest.mock import Mock
m = Mock()
m.send("hello", priority=1)
m.send("world", priority=5)
# Various assertions
m.send.assert_called() # at least once
m.send.assert_called_once() # exactly once — fails if called twice
m.send.assert_called_with("world", priority=5) # last call matched
m.send.assert_any_call("hello", priority=1) # any call matched
# Inspect the full history
print(m.send.call_args_list)
# [call('hello', priority=1), call('world', priority=5)]
Write a function get_weather(city) in a module weather.py that imports requests and calls requests.get(url).json(). Write test_weather.py that uses patch("weather.requests.get") to mock the response — return a fake JSON dict. Verify (a) the test doesn't make a network call, (b) you can assert the URL passed to requests.get, (c) you can simulate a requests.RequestException via side_effect. Add a second test using monkeypatch instead of patch and compare.
Progress
Progress is local-only — sign in to sync across devices.