Test Doubles: Mocks, Stubs and Fakes
Replacing real dependencies in tests: dummies, stubs, spies, mocks and fakes, unittest.mock and monkeypatch, and why too many mocks make tests brittle.
Why tests replace dependencies
The Portal team's cost page needs four things besides its own logic: hourly prices from a market price service (for customers on a dynamic contract), the 15-minute readings from the reading repository (PostgreSQL in production), a notifier that sends an SMS or email when a reading looks wrong, and an audit log. A unit test that uses all four for real would be slow, would fail whenever the price service is down, could send a real SMS to a real customer, and could not easily produce the situation it wants to test, such as a price of exactly €0.1910 at 23:00 or a timeout.
A test double is an object that takes the place of such a dependency during a test. The term, after a stunt double in a film, comes from Gerard Meszaros' book xUnit Test Patterns (2007). The Java page shows the idea with a hand-written class; this page covers the kinds of double, the Python tools, and when doubles help or hurt.
Replacing a dependency is easiest when the code receives it from
outside instead of creating it itself: DailyCost gets
its prices, readings, notifier and audit log through its
constructor. That is
dependency injection, and it is the main reason testable code looks the way it does.
The five kinds of test double
Martin Fowler's summary of Meszaros' vocabulary (his "TestDouble" article of 2006, and "Mocks Aren't Stubs", first published in 2004) names five kinds. In everyday speech people call all of them "mocks", which causes real misunderstandings in reviews; the difference is in what the double does and what the test checks. Here are four of them in one test file, written by hand as ordinary Python classes:
from datetime import date, datetime
from decimal import ROUND_HALF_UP, Decimal
# ---- code under test ----------------------------------------------------------
class DailyCost:
"""Cost of one day for a customer on a dynamic (hourly price) contract."""
def __init__(self, prices, readings, notifier, audit_log):
self.prices = prices # market price service, over the network
self.readings = readings # reading repository, PostgreSQL in production
self.notifier = notifier # sends an SMS or email to the customer
self.audit_log = audit_log # only used when a price is corrected
def total(self, meter_id, day):
total = Decimal("0")
for at, kwh in self.readings.for_day(meter_id, day):
if kwh > 25:
self.notifier.send(meter_id, f"Unusual reading: {kwh} kWh at {at:%H:%M}")
total += Decimal(str(kwh)) * self.prices.price_at(at)
return total.quantize(Decimal("0.01"), ROUND_HALF_UP)
# ---- test doubles -------------------------------------------------------------
class StubPrices:
"""Stub: canned hourly prices instead of a call to the market."""
CANNED = {14: Decimal("0.2850"), 18: Decimal("0.3410"), 23: Decimal("0.1910")}
def price_at(self, at):
return self.CANNED[at.hour]
class FakeReadings:
"""Fake: a working repository that keeps rows in a dict."""
def __init__(self):
self.rows = {}
def add(self, meter_id, at, kwh):
self.rows.setdefault(meter_id, []).append((at, kwh))
def for_day(self, meter_id, day):
return [(at, kwh) for at, kwh in self.rows.get(meter_id, []) if at.date() == day]
class SpyNotifier:
"""Spy: sends nothing, but remembers every call."""
def __init__(self):
self.sent = []
def send(self, meter_id, message):
self.sent.append((meter_id, message))
UNUSED_AUDIT_LOG = object() # Dummy: the constructor needs it, these tests never use it
# ---- tests --------------------------------------------------------------------
def test_each_reading_is_priced_at_its_own_hour():
readings = FakeReadings()
readings.add("M-30007", datetime(2026, 10, 5, 14, 15), 1.0)
readings.add("M-30007", datetime(2026, 10, 5, 23, 15), 1.5)
readings.add("M-30007", datetime(2026, 10, 6, 0, 15), 2.0) # next day
daily_cost = DailyCost(StubPrices(), readings, SpyNotifier(), UNUSED_AUDIT_LOG)
assert daily_cost.total("M-30007", date(2026, 10, 5)) == Decimal("0.57")
def test_customer_is_warned_about_a_spike():
readings = FakeReadings()
readings.add("M-30001", datetime(2026, 10, 5, 18, 0), 0.4)
readings.add("M-30001", datetime(2026, 10, 5, 18, 15), 91.3)
notifier = SpyNotifier()
DailyCost(StubPrices(), readings, notifier, UNUSED_AUDIT_LOG).total("M-30001", date(2026, 10, 5))
assert notifier.sent == [("M-30001", "Unusual reading: 91.3 kWh at 18:15")]
.. [100%] 2 passed in 0.16s
The first test expects €0.57: 1.0 kWh at €0.2850 plus 1.5 kWh at €0.1910 is €0.5715; the reading after midnight belongs to the next day. The prices are made up for the test, which is the point: the stub decides them, so the expected result can be worked out in advance.
Dummy
A dummy object is passed around
only to fill a required parameter and is never actually used.
UNUSED_AUDIT_LOG = object() is one: the constructor
wants an audit log, but these tests never correct a price. If the
code under test does use a dummy by mistake, the test fails loudly,
because a bare object() has no methods. That is useful
information too.
Stub
A stub provides canned
answers to the calls made during the test and nothing else.
StubPrices returns a fixed price per hour instead of
asking the market. Stubs let you put the code under test in a
situation that is hard to create for real: a negative market price
on a sunny afternoon, a missing hour, a timeout. The test then
checks the result of the code under test, not the stub.
Spy
A test spy is a stub that also
records how it was called, so the test can inspect the calls
afterward. SpyNotifier sends nothing and keeps a list
of messages; the second test checks that list. Use a spy when the
visible effect of the code is a call to something else, such as "the
customer gets one warning", which has no return value to check.
Mock
A mock object is set up in
advance with expectations about which calls it should receive, and
it verifies those calls: the test fails if a call is missing,
unexpected or has the wrong arguments. The difference with a spy is
subtle and mostly in the style: with a spy you look at what happened
afterward; with a mock, the expected interaction is the
specification. Mocking libraries such as Python's
unittest.mock and Java's
Mockito
create objects that can act as stub, spy or mock depending on how
you use them, which is why the word "mock" is used so loosely.
Fake
A fake object has a real, working
implementation, but takes a shortcut that makes it unsuitable for
production. FakeReadings stores rows in a dictionary
instead of PostgreSQL; an in-memory SQLite database, a fake SMTP
server that keeps emails in a list, or a local file instead of cloud
storage are other fakes. A fake behaves consistently (what you add,
you can read back), so tests can use it naturally, and one good fake
serves hundreds of tests. Its risk is that it drifts from the real
thing; a few
integration tests
against the real database keep it honest.
| Kind | Answers calls | Records calls | Checks calls | Cost page example |
|---|---|---|---|---|
| Dummy | No | No | No | The audit log in a pricing test |
| Stub | Canned | No | No | Fixed hourly prices |
| Spy | Canned | Yes | The test does, afterward | The notifier's list of messages |
| Mock | Canned | Yes | Yes, against expectations | "send must be called once with M-30001" |
| Fake | Really computed | Has real state | No | In-memory reading repository |
Python's unittest.mock
unittest.mock is the
standard-library module for creating test doubles on the fly. Its
Mock class accepts any attribute access and any call,
returns another mock, and records everything; you configure what it
returns (return_value), what it raises or returns over
successive calls (side_effect), and afterward ask how
it was called (call_count, call_args,
call_args_list, and assertion methods such as
assert_called_once_with). MagicMock is the
same with Python's special methods (len(), iteration,
with) preconfigured. It works with pytest and with
unittest alike:
from unittest.mock import Mock, call, create_autospec
class Notifier:
def send(self, meter_id, message):
"""Send an SMS or email to the customer (needs the SMS gateway)."""
def warn_about_spikes(readings, notifier):
for meter_id, kwh in readings:
if kwh > 25:
notifier.send(meter_id, f"Unusual reading: {kwh} kWh")
# A Mock accepts any call and records it
notifier = Mock()
warn_about_spikes([("M-30001", 0.4), ("M-30001", 91.3), ("M-30002", 30.0)], notifier)
print("calls:", notifier.send.call_count)
print(notifier.send.call_args_list == [
call("M-30001", "Unusual reading: 91.3 kWh"),
call("M-30002", "Unusual reading: 30.0 kWh"),
])
notifier.send.assert_called_with("M-30002", "Unusual reading: 30.0 kWh")
print("last call checked")
# return_value and side_effect give canned answers
prices = Mock()
prices.price_at.return_value = 0.224
print(prices.price_at("23:15"))
prices.price_at.side_effect = [0.31, 0.29, TimeoutError("price service")]
print(prices.price_at("07:00"), prices.price_at("08:00"))
try:
prices.price_at("09:00")
except TimeoutError as error:
print("TimeoutError:", error)
# A plain Mock also accepts a method that does not exist
notifier.sned("M-30001", "typo")
print("sned accepted")
# An autospec mock only accepts what the real class has
strict = create_autospec(Notifier, instance=True)
try:
strict.sned("M-30001", "typo")
except AttributeError as error:
print("AttributeError:", error)
try:
strict.send("M-30001")
except TypeError as error:
print("TypeError:", error)
calls: 2 True last call checked 0.224 0.31 0.29 TimeoutError: price service sned accepted AttributeError: Mock object has no attribute 'sned' TypeError: missing a required argument: 'message'
The last part is the most important. A plain Mock is
happy with notifier.sned(...), a method that does not
exist, so a typo in the code under test passes the test.
create_autospec (or autospec=True when
patching, below) builds the mock from the real class: unknown
attributes raise AttributeError, and calls with the
wrong arguments raise TypeError, just like the real
object would. Prefer it whenever there is a real class to copy.
Patching: replace the name where it is looked up
When the code under test does not accept its dependency as a
parameter, a test can temporarily replace it with
unittest.mock.patch: inside the with block
(or the decorated test), the given name points to a mock, and
afterward the original comes back. The rule that trips everyone up:
patch the name where the code under test looks it up, not
where it was defined. Here cost_page.py does
from prices import fetch_hourly_prices, which copies
the name into the cost_page module at import time:
cat > prices.py <<'PY'
def fetch_hourly_prices(day):
"""Ask the market price service (network) for 24 hourly prices."""
raise RuntimeError("network call made during a test")
PY
cat > cost_page.py <<'PY'
from prices import fetch_hourly_prices
def cheapest_hour(day):
prices = fetch_hourly_prices(day)
return prices.index(min(prices))
PY
cat > test_cost_page.py <<'PY'
from unittest.mock import patch
import cost_page
PRICES = [0.30] * 24
PRICES[3] = 0.12
def test_patch_where_it_is_defined():
with patch("prices.fetch_hourly_prices", return_value=PRICES):
assert cost_page.cheapest_hour("2026-10-07") == 3
def test_patch_where_it_is_looked_up():
with patch("cost_page.fetch_hourly_prices", return_value=PRICES):
assert cost_page.cheapest_hour("2026-10-07") == 3
PY
pytest -q --tb=short -p no:cacheprovider
F. [100%]
=================================== FAILURES ===================================
________________________ test_patch_where_it_is_defined ________________________
test_cost_page.py:11: in test_patch_where_it_is_defined
assert cost_page.cheapest_hour("2026-10-07") == 3
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
cost_page.py:5: in cheapest_hour
prices = fetch_hourly_prices(day)
^^^^^^^^^^^^^^^^^^^^^^^^
prices.py:3: in fetch_hourly_prices
raise RuntimeError("network call made during a test")
E RuntimeError: network call made during a test
=========================== short test summary info ============================
FAILED test_cost_page.py::test_patch_where_it_is_defined - RuntimeError: netw...
1 failed, 1 passed in 0.17s
The first test replaced prices.fetch_hourly_prices, but
cost_page still holds its own reference to the original
function, so the real (here: deliberately failing) network call ran.
The second test patched cost_page.fetch_hourly_prices,
the name the code actually uses, and passed. Had
cost_page.py written import prices and
called prices.fetch_hourly_prices(day), patching
prices.fetch_hourly_prices would have worked, because
the lookup then happens in the prices module at call
time.
Monkeypatching with pytest
Monkeypatching is replacing an
attribute, function, class or environment variable at run time, from
outside the code that uses it. In tests it must always be undone
afterward, or one test changes the world for the next. pytest's
built-in monkeypatch fixture does that for you:
setattr, setenv, delenv and
chdir are all reverted when the test ends, whether it
passed or failed.
A classic use is the clock. "Is it night now?" depends on when the test runs, which makes a test that passes in the afternoon and fails at 23:30. Compare patching the clock with passing it in:
import os
from datetime import datetime
def is_night(at):
return at.hour < 7 or at.hour >= 23
def current_tariff():
"""Hard to test: reads the real clock."""
return "night" if is_night(datetime.now()) else "day"
def tariff_at(clock):
"""Easy to test: the caller passes in the clock."""
return "night" if is_night(clock()) else "day"
def price_api_url():
return os.environ.get("PRICE_API_URL", "https://api.kestrel.example/prices/v1")
def test_current_tariff_late_evening(monkeypatch):
class FrozenDatetime(datetime):
@classmethod
def now(cls, tz=None):
return cls(2026, 10, 5, 23, 15)
monkeypatch.setattr("test_example.datetime", FrozenDatetime)
assert current_tariff() == "night"
def test_tariff_with_an_injected_clock():
assert tariff_at(lambda: datetime(2026, 10, 5, 23, 15)) == "night"
def test_price_api_from_the_environment(monkeypatch):
monkeypatch.setenv("PRICE_API_URL", "http://127.0.0.1:8099/v1")
assert price_api_url() == "http://127.0.0.1:8099/v1"
def test_default_price_api(monkeypatch):
monkeypatch.delenv("PRICE_API_URL", raising=False)
assert price_api_url() == "https://api.kestrel.example/prices/v1"
============================= test session starts ============================== collecting ... collected 4 items test_example.py::test_current_tariff_late_evening PASSED [ 25%] test_example.py::test_tariff_with_an_injected_clock PASSED [ 50%] test_example.py::test_price_api_from_the_environment PASSED [ 75%] test_example.py::test_default_price_api PASSED [100%] ============================== 4 passed in 0.17s ===============================
Both clock tests pass, but look at the effort. The first one has to
know that current_tariff uses the name
datetime in this module, and replace that whole class
with a subclass, because the built-in datetime.now
cannot be patched directly. The second one just passes a function
that returns a fixed time. That is
dependency injection
again, and it is the better design: when a test needs patching, the
code is often telling you that a dependency is hidden.
Monkeypatching is the right tool for things you cannot easily pass
in, such as
environment variables
(the last two tests) or a third-party module's internals.
State or behavior: two schools
There are two ways to check that code did the right thing. State verification looks at the result: the returned total, the rows in the fake repository, the messages the spy collected. Behavior verification checks the interactions: the price service was asked for hour 23, the notifier was called once with these arguments. Fowler's "Mocks Aren't Stubs" describes the two styles:
- Classicist (also "Detroit" or "Chicago" school): use real objects where possible, fakes and stubs where not, and verify state. Tests survive refactoring well, because they do not care how the result was reached.
- Mockist ("London" school, after the London XP community where mock objects were invented; its best-known book is Growing Object-Oriented Software, Guided by Tests by Steve Freeman and Nat Pryce, 2009): mock every collaborator and verify the interactions. This drives the design of the interfaces between objects, but ties the tests to them.
Most teams mix the two: state verification by default, and a mock or spy where the interaction is the behavior, as with "send exactly one warning" or "never call the payment provider twice".
Over-mocking
Over-mocking is using so many mocks, or mocks so detailed, that tests mirror the implementation instead of checking behavior. It hurts in two opposite ways.
Tests that break for nothing
Bram Peeters rewords the warning to be friendlier. The customer still gets exactly one warning about the right meter and the right amount, but the test that pinned the exact text fails:
from unittest.mock import Mock, call
def warn_about_spikes(readings, notifier):
for meter_id, kwh in readings:
if kwh > 25:
notifier.send(meter_id, f"Unusual reading of {kwh} kWh, please check your meter")
def test_customer_is_warned_once_about_a_spike():
notifier = Mock()
warn_about_spikes([("M-30001", 0.4), ("M-30001", 91.3)], notifier)
assert notifier.send.call_args_list == [call("M-30001", "Unusual reading: 91.3 kWh")]
def test_customer_is_warned_once_about_a_spike_robust():
notifier = Mock()
warn_about_spikes([("M-30001", 0.4), ("M-30001", 91.3)], notifier)
assert notifier.send.call_count == 1
meter_id, message = notifier.send.call_args.args
assert meter_id == "M-30001"
assert "91.3 kWh" in message
F. [100%]
=================================== FAILURES ===================================
__________________ test_customer_is_warned_once_about_a_spike __________________
def test_customer_is_warned_once_about_a_spike():
notifier = Mock()
warn_about_spikes([("M-30001", 0.4), ("M-30001", 91.3)], notifier)
> assert notifier.send.call_args_list == [call("M-30001", "Unusual reading: 91.3 kWh")]
E AssertionError: assert [call('M-3000... your meter')] == [call('M-3000...g: 91.3 kWh')]
E
E At index 0 diff: call('M-30001', 'Unusual reading of 91.3 kWh, please check your meter') != call('M-30001', 'Unusual reading: 91.3 kWh')
E Use -v to get more diff
test_example.py:13: AssertionError
=========================== short test summary info ============================
FAILED test_example.py::test_customer_is_warned_once_about_a_spike - Assertio...
1 failed, 1 passed in 0.25s
The second test checks what matters (one warning, the right meter, the amount) and survives the rewording. Multiply the first style by a few hundred tests, each asserting every call with every argument, and every refactoring turns into an afternoon of updating tests that found nothing wrong. Teams then learn to update failing tests without reading them, and the suite stops protecting anything.
Mocks that lie
The other danger is a test that passes while the real thing is
broken. A mock only knows what the test told it. In version 2 of the
price client, price_at was renamed to
hourly_price, but the cost code was not updated:
from unittest.mock import Mock, create_autospec
class PriceService:
"""The real client. Version 2 renamed price_at to hourly_price."""
def hourly_price(self, day, hour):
raise RuntimeError("calls the market price API")
def cost_at(prices, day, hour, kwh):
return round(kwh * prices.price_at(day, hour), 2) # still uses the old name
def test_with_a_plain_mock():
prices = Mock()
prices.price_at.return_value = 0.20
assert cost_at(prices, "2026-10-07", 18, 1.5) == 0.30
def test_with_an_autospec_mock():
prices = create_autospec(PriceService, instance=True)
prices.price_at.return_value = 0.20
assert cost_at(prices, "2026-10-07", 18, 1.5) == 0.30
for test in (test_with_a_plain_mock, test_with_an_autospec_mock):
try:
test()
print(f"{test.__name__}: passed")
except Exception as error:
print(f"{test.__name__}: failed with {type(error).__name__}: {error}")
test_with_a_plain_mock: passed test_with_an_autospec_mock: failed with AttributeError: Mock object has no attribute 'price_at'
(The two tests are called by hand here, so that the output shows
both outcomes side by side; under pytest the second would be a
normal failure.) The plain mock happily invents
price_at, so the test is green while production would
crash on the first dynamic-price customer. The autospec mock follows
the real class and catches it. Autospec only helps when the class is
in your code base, though. For a service on the other side of a
network, such as meter-api or the market price API, a mock can only
repeat your assumptions about its responses;
contract tests
check those assumptions against the real provider.
A rule of thumb from the mockist school itself is "don't mock what
you don't own": instead of mocking a third-party client library
directly, wrap it in a small class of your own (a
PriceService with the one method you need), mock or
fake that in unit tests, and test the wrapper against the
real library or service in a few integration tests.
Choosing a test double
| Situation | Use |
|---|---|
| The dependency is fast and has no side effects (a price calculation, a parser) | The real object; no double |
| You need specific answers from a slow or external service | A stub |
| Many tests store and read data | A fake, checked by a few integration tests |
| The call itself is the behavior (a warning, a payment) | A spy or mock, checking only what matters |
| The code reads the clock, randomness or environment | Pass it in; otherwise monkeypatch |
| A mock of a class from your own code |
create_autospec or autospec=True
|
| The service is on the other side of a network | A stub or mock server in unit tests, plus contract tests |