🎭 Mocking and Patching in Python Tests
Real code talks to APIs, databases, files, and the clock — dependencies that are slow, flaky, or impossible to control from a test. Mocking replaces those dependencies with stand-ins you fully command, so you can isolate the code under test. This lesson covers Python's unittest.mock end to end: building mocks, the make-or-break rule of where to patch, verifying calls, and the anti-patterns that quietly ruin a suite.
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Explain what mocking is and choose between Mock, MagicMock, and autospec
- Configure mocks with return values and side effects (exceptions, sequences, functions)
- Apply the rule "patch where an object is used, not where it's defined"
- Verify interactions with mock assertions like
assert_called_once_with - Mock HTTP, files, and time, and recognize mocking anti-patterns
Estimated Time: 50–60 minutes • Difficulty: Intermediate
Hands-on: Mock every external dependency of a weather-service client.
In This Lesson
What Is Mocking?
A mock is a fake object that stands in for a real one during a test. You tell it what to return and what to raise, and afterward you can ask it how it was called. Mocking lets you isolate the unit under test from the messy outside world — a payment gateway, a REST API, a database, the filesystem, or the system clock.
💡 Analogy: Mocking is a flight simulator for your code. Instead of risking a real aircraft (a live API, real money, a production database), you run in a controlled environment that mimics real conditions — including the failures you could never trigger on demand, like a timeout or a 500 error.
The unittest.mock Library
Mocking is in the standard library (unittest.mock, since Python 3.3) — no install required. Its main pieces:
| Tool | What it does |
|---|---|
Mock | A flexible fake; every attribute/method access auto-creates another Mock |
MagicMock | Like Mock but also supports magic methods (len(), [], with, +) |
patch | Temporarily replaces a target object for the duration of a test |
AsyncMock | A mock whose calls are awaitable, for async code |
call / ANY | Helpers to describe and match calls in assertions |
from unittest.mock import Mock, MagicMock
# A plain Mock records how it's used
mock = Mock()
mock.method("hi")
assert mock.method.called
# MagicMock supports magic methods a plain Mock does not
magic = MagicMock()
len(magic) # works: returns 0 by default
magic[0] # works: returns another MagicMock
with magic: # works: supports the context-manager protocol
pass
📖 Mock vs MagicMock
Use Mock for ordinary objects and functions. Reach for MagicMock when the code under test uses dunder (magic) methods on the dependency — indexing, iteration, len(), arithmetic, or the with statement. When in doubt, MagicMock is the safe default.
Configuring Mocks
Return values
from unittest.mock import Mock
mock = Mock(return_value=42)
assert mock() == 42
# Configure a method's return value
mock.get_user.return_value = {"id": 1, "name": "Ray"}
assert mock.get_user()["name"] == "Ray"
Side effects
side_effect unlocks richer behavior. Set it to an exception to raise, a list to return successive values, or a function to compute the result:
from unittest.mock import Mock
# 1. Raise an exception
mock = Mock(side_effect=ValueError("boom"))
# mock() now raises ValueError("boom")
# 2. Return a different value on each call
mock = Mock(side_effect=[1, 2, 3])
assert [mock(), mock(), mock()] == [1, 2, 3]
# 3. Delegate to a function
def double_or_reject(x):
if x < 0:
raise ValueError("negative")
return x * 2
mock = Mock(side_effect=double_or_reject)
assert mock(5) == 10
spec and autospec — safer mocks
A bare Mock accepts any attribute, so a typo like mock.grret() silently passes. A spec constrains the mock to the real object's interface; autospec goes further and also checks the call signatures:
from unittest.mock import Mock, create_autospec
class Person:
def greet(self, other):
return f"Hi {other}"
# spec: only real attributes are allowed
mock = Mock(spec=Person)
mock.greet("Ana") # OK
# mock.dance() # AttributeError — Person has no `dance`
# autospec: also enforces the signature
auto = create_autospec(Person)
# auto.greet() # TypeError — missing the `other` argument
✅ Prefer autospec for public interfaces
Autospec catches the sneakiest failure mode in mocking: your test keeps passing after you rename or change the signature of the real method, because a bare mock happily accepts the old call. With autospec, the mock changes when the real API changes.
Patching & Where to Patch
patch swaps out a target for a mock, then restores the original when the test ends. It works as a decorator, a context manager, or manually:
# app.py
import requests
def get_user(user_id):
resp = requests.get(f"https://api.example.com/users/{user_id}")
return resp.json() if resp.status_code == 200 else None
# test_app.py
from unittest.mock import patch, Mock
from app import get_user
@patch("app.requests.get") # decorator form
def test_get_user(mock_get):
mock_get.return_value = Mock(status_code=200,
json=Mock(return_value={"id": 1, "name": "Ray"}))
assert get_user(1) == {"id": 1, "name": "Ray"}
mock_get.assert_called_once_with("https://api.example.com/users/1")
def test_as_context_manager():
with patch("app.requests.get") as mock_get: # context-manager form
mock_get.return_value = Mock(status_code=404)
assert get_user(999) is None
The golden rule: patch where it's used
This trips up nearly everyone. You must patch the name in the module that uses it — not the module where it was originally defined. Because from x import y creates a new reference y inside the importing module, patching the original x.y leaves that reference untouched.
⚠️ The most common mocking mistake
# services.py
import requests
def fetch(url):
return requests.get(url).json()
# consumer.py
from services import fetch # `fetch` now lives in consumer's namespace
def process(user_id):
return fetch(f"https://api.example.com/users/{user_id}")
# WRONG — patches the original, not the reference consumer.py uses
@patch("services.fetch")
def test_wrong(mock_fetch): ... # process() still calls the real fetch!
# RIGHT — patch the name where it is looked up
@patch("consumer.fetch")
def test_right(mock_fetch):
mock_fetch.return_value = {"name": "Ray"}
assert process(1) == {"name": "Ray"}
The tell: if you did from services import fetch in consumer.py, patch consumer.fetch. If instead you did import services and call services.fetch(), patch services.fetch. Patch the name at the location where the code looks it up.
Stacked patches and patch.object
# Decorators apply bottom-up, so arguments read inside-out
@patch("app.logging") # -> outer arg
@patch("app.requests.get") # -> inner arg (closest to the function)
def test_two_patches(mock_get, mock_logging):
...
# Patch a single attribute/method of a class
from unittest.mock import patch
@patch.object(UserService, "get_user", return_value={"name": "Ray"})
def test_object_patch(mock_get_user):
...
# Temporarily set environment variables
@patch.dict("os.environ", {"API_KEY": "test-key"})
def test_env(): ...
Mock Assertions
Every mock records its calls, so you can verify your code interacted with its dependencies correctly:
from unittest.mock import Mock, call, ANY
mock = Mock()
mock(1, 2, key="value")
mock.assert_called() # called at least once
mock.assert_called_once() # called exactly once
mock.assert_called_with(1, 2, key="value") # most recent call matched these args
mock.assert_called_once_with(1, 2, key="value")
# Never called?
other = Mock()
other.assert_not_called()
# Inspect call history
mock(3)
assert mock.call_count == 2
assert mock.call_args_list == [call(1, 2, key="value"), call(3)]
# Don't care about some args? Use ANY
mock.assert_called_with(ANY, ANY, key="value")
For a sequence of method calls, assert_has_calls checks that they occurred (in order by default):
service = Mock()
service.process(1)
service.process(2)
service.finish("batch1")
service.assert_has_calls([
call.process(1),
call.process(2),
call.finish("batch1"),
])
⚠️ Beware the typo trap
A bare mock will happily accept a misspelled assertion like mock.asssert_called() and do nothing — the test passes without checking anything. Autospec'd mocks reject unknown attributes, so combining autospec with real assertions is the safest habit.
Common Scenarios
HTTP requests
import requests
from unittest.mock import patch, Mock
def get_user_data(user_id):
resp = requests.get(f"https://api.example.com/users/{user_id}")
return resp.json() if resp.status_code == 200 else None
@patch("app.requests.get")
def test_get_user_data(mock_get):
mock_get.return_value = Mock(status_code=200,
json=Mock(return_value={"id": 1, "name": "Ray"}))
assert get_user_data(1) == {"id": 1, "name": "Ray"}
mock_get.return_value = Mock(status_code=404)
assert get_user_data(999) is None
💡 For HTTP, consider the responses library
Manually mocking requests gets verbose. The third-party responses library lets you register fake endpoints declaratively (responses.add(responses.GET, url, json=..., status=200)), which reads more clearly for API-heavy code.
File operations with mock_open
from unittest.mock import patch, mock_open
def read_first_line(path):
with open(path) as f:
return f.readline().strip()
@patch("builtins.open", new_callable=mock_open, read_data="hello\nworld")
def test_read_first_line(mock_file):
assert read_first_line("data.txt") == "hello"
mock_file.assert_called_once_with("data.txt")
Time and randomness
Time-dependent code is untestable until you control the clock. Patch time.time with a side_effect list to script successive readings:
from unittest.mock import patch
def elapsed(func):
import time
start = time.time()
func()
return time.time() - start
@patch("app.time.time")
def test_elapsed(mock_time):
mock_time.side_effect = [1000.0, 1030.0] # start, then end
assert elapsed(lambda: None) == 30.0
Async code with AsyncMock
import pytest
from unittest.mock import AsyncMock, patch
@patch("app.fetch_data", new_callable=AsyncMock)
async def test_async(mock_fetch):
mock_fetch.return_value = {"data": "test"}
result = await process("https://example.com/api")
mock_fetch.assert_awaited_once_with("https://example.com/api")
Anti-Patterns
Mocking is sharp — used carelessly it produces tests that pass while the real code is broken. Watch for these:
| Anti-pattern | Why it hurts |
|---|---|
| Over-mocking — mocking every collaborator | The test only exercises the interaction of your mocks, not real behavior |
| Mocking the system under test | Patching the method you're testing means you test the mock, not the code |
| Asserting implementation details | Tests break on harmless refactors even though outputs are still correct |
| Bare mocks without spec | Typos and stale signatures pass silently; use autospec |
# Anti-pattern: mocking what you're trying to test
@patch("module.MyClass.method_under_test")
def test_useless(mock_method):
mock_method.return_value = "expected"
assert MyClass().method_under_test() == "expected" # tests nothing real
# Anti-pattern: over-asserting the implementation
@patch("module.helper")
def test_brittle(mock_helper):
process_data([1, 2, 3])
mock_helper.assert_called_with([1, 2, 3]) # breaks on any refactor
# Better: assert the outcome
def test_outcome():
assert process_data([1, 2, 3]) == [2, 4, 6]
⚠️ A cautionary tale
Over-mocking can hide real bugs: a suite passes because the mocks were told to return the expected values, while the actual code path — the one that runs in production — is broken. Mock the boundaries of your system (network, disk, clock), and let your own logic run for real.
An alternative: dependency injection
Often the cleanest way to make code testable is to pass in its dependencies instead of hard-coding them. Then a test simply supplies a fake — no patching needed:
# Hard to test: dependency is created inside
def process():
client = ApiClient()
return client.get_data()
# Easy to test: dependency is injected
def process(client=None):
client = client or ApiClient()
return client.get_data()
def test_process():
fake = Mock()
fake.get_data.return_value = "data"
assert process(client=fake) == "data"
Hands-on Exercise
🏋️ Mock a weather service
Objective: Test a client that calls an external API — without ever hitting the network. Here is the code:
# weather.py
import requests
class WeatherService:
def __init__(self, api_key):
if not api_key:
raise ValueError("API key is required")
self.api_key = api_key
self.base_url = "https://api.weatherapi.com/v1"
def current(self, city):
resp = requests.get(f"{self.base_url}/current.json",
params={"key": self.api_key, "q": city})
if resp.status_code != 200:
return {"error": f"Failed: {resp.status_code}"}
data = resp.json()
return {"city": data["location"]["name"],
"temp_c": data["current"]["temp_c"]}
Your tasks
- Patch
weather.requests.getand test a successfulcurrent("London")call returns the parsed dict. - Assert
requests.getwas called with the correct URL andparams. - Test the error path: a non-200 status returns the
{"error": ...}dict. - Verify the constructor raises
ValueErrorwhen the API key is empty.
💡 Hint
Remember to patch where it's used: @patch("weather.requests.get"). Build the fake response with Mock(status_code=200, json=Mock(return_value={...})). Check the call args with mock_get.assert_called_once_with(url, params={...}).
✅ Sample solution
# test_weather.py
import pytest
from unittest.mock import patch, Mock
from weather import WeatherService
SAMPLE = {"location": {"name": "London"}, "current": {"temp_c": 12.0}}
@patch("weather.requests.get")
def test_current_success(mock_get):
mock_get.return_value = Mock(status_code=200,
json=Mock(return_value=SAMPLE))
service = WeatherService("key123")
result = service.current("London")
assert result == {"city": "London", "temp_c": 12.0}
mock_get.assert_called_once_with(
"https://api.weatherapi.com/v1/current.json",
params={"key": "key123", "q": "London"},
)
@patch("weather.requests.get")
def test_current_error(mock_get):
mock_get.return_value = Mock(status_code=500)
service = WeatherService("key123")
assert service.current("London") == {"error": "Failed: 500"}
def test_requires_api_key():
with pytest.raises(ValueError, match="API key is required"):
WeatherService("")
Notice the network is never touched — the tests are fast, deterministic, and can even exercise the 500 path you could never reliably trigger against the real API.
Best Practices
| ✅ Do | 🚫 Avoid |
|---|---|
| Mock the boundaries: network, disk, clock, randomness | Mocking your own logic that you're trying to verify |
| Patch where the name is used | Patching where it was originally defined |
Use autospec/spec to keep mocks honest | Bare mocks that accept any attribute or signature |
| Assert on outcomes; verify calls only when the interaction is the contract | Pinning down every internal helper call |
| Prefer dependency injection when it makes code cleaner | Reaching for patch to work around poor design |
💡 The goal is isolation, not avoidance. Mock to remove uncontrollable dependencies so your logic can be tested — not to sidestep testing real behavior. If a test is nothing but mocks talking to mocks, it isn't testing your code.
✅ pytest-mock makes this tidier
Under pytest, the pytest-mock plugin provides a mocker fixture: mock_get = mocker.patch("app.requests.get"). It wraps unittest.mock and undoes every patch automatically at the end of the test — no decorators or with blocks to manage.
Summary & Quiz
🎉 Key Takeaways
- A mock is a controllable stand-in that also records how it was called;
MagicMockadds magic-method support. - Configure behavior with
return_valueandside_effect; usespec/autospecto keep mocks faithful to the real interface. - Patch where an object is used, not where it's defined — the single most common mocking mistake.
- Verify interactions with
assert_called_once_with,call_args_list, andANY. - Mock the boundaries (HTTP, files, time, async) and avoid over-mocking, which hides real bugs.
🎯 Quick Quiz
Question 1: In consumer.py you wrote from services import fetch. Which target should you patch?
Question 2: You need a mock to return 1, then 2, then 3 on successive calls. What do you set?
Question 3: Why is over-mocking dangerous?
📚 Further Reading
- Python docs — unittest.mock
- Python docs — Where to patch
- pytest-mock — the mocker fixture
- responses — mocking the requests library
🚀 What's Next?
You've now covered testing across the Python ecosystem. Next we cross into the PHP world and apply the same testing mindset — assertions, isolation, and structure — in a new language with PHPUnit Testing Framework.
🎉 Nice work!
You can now isolate any unit of code from the messy world around it — and prove it talks to that world correctly.