PYTH-230 · Lesson 4 of 12 · 27 min
Mocking and test doubles
▶ Video coming soon A video version of this lesson is on the way.
Some of Ridgeline's code is easy to test because it only does arithmetic on the values passed in. Other code reaches out into the world. The daily low-stock alert sends an email to Theo. The sell-by labels depend on today's date. The coffee-of-the-week picker uses random numbers. The online order import calls the shop's web service over the internet. A test that really sends email, waits for a particular date, depends on luck or needs the shop to be online will be slow, flaky or impossible to run on a laptop on a train.
The solution is to replace those outside pieces, during the test, with stand-ins that the test controls. A stand-in object used in place of a real one in a test is called a test double, after the stunt doubles who replace actors in dangerous scenes. This lesson shows how to design code so its dependencies can be swapped, the different kinds of doubles and when to use each, and how to use the standard library's unittest.mock module. It ends with a warning about overdoing it.
Make dependencies replaceable
Before you can swap a dependency, the code has to let you. The easiest way is to pass the dependency in as an argument instead of reaching for it from inside the function. This is called dependency injection, and you already met it in PYTH-102, where the Inventory class accepted a clock argument. Compare the two versions of the sell-by label function below.
from datetime import date, timedelta
def label_hard_to_test(roast_date):
"""Reaches for today's date itself: the result changes every day."""
days_left = (roast_date + timedelta(days=30) - date.today()).days
return "SELL SOON" if days_left <= 5 else f"{days_left} days left"
def label(roast_date, today=None):
"""Takes today's date as an argument, defaulting to the real one."""
if today is None:
today = date.today()
days_left = (roast_date + timedelta(days=30) - today).days
return "SELL SOON" if days_left <= 5 else f"{days_left} days left"
roasted = date(2026, 11, 2)
print(label(roasted, today=date(2026, 11, 10)))
print(label(roasted, today=date(2026, 11, 28)))
print(label(roasted, today=date(2026, 12, 2)))22 days left SELL SOON SELL SOON
The first version gives a different answer every day, so any test of it either breaks tomorrow or cannot check a specific result. The second version behaves identically in the real program, which simply omits the argument, but a test can hand it any date it likes and check the exact label. A small change in design turns untestable code into code that is easy to test. Code that is hard to test is often a sign of a design problem: it is doing too many things, or depending on too much.
The same idea works for any dependency: pass in the mailer, the random number generator, the shop client or the file path. In a class, accept the dependency in __init__ and store it, as Inventory did with its clock. The real program wires in the real objects once, usually in main, and every other part of the code just uses whatever it was given.
Kinds of test doubles
Test doubles come in several flavors, and the names are worth knowing because developers use them constantly. The terms come from Gerard Meszaros's book xUnit Test Patterns.
A hand-written fake is often the clearest double. Here is one for the low-stock alert. The real mailer connects to a mail server; the fake just appends each message to a list, which the test then inspects. Because the alert function receives its mailer as an argument, swapping one for the other needs no tricks at all.
import unittest
def report(*cases):
"""Run TestCase classes and print one line per test (a small test runner)."""
loader = unittest.TestLoader()
tests = [t for case in cases for t in loader.loadTestsFromTestCase(case)]
result = unittest.TestResult()
unittest.TestSuite(tests).run(result)
problems = {}
for kind, items in (("FAIL", result.failures), ("ERROR", result.errors)):
for test, tb in items:
problems.setdefault(test.id().split(" ")[0], []).append((kind, tb.strip().splitlines()[-1]))
for t in tests:
found = problems.get(t.id(), [])
print(f"{t.id().rsplit('.', 1)[-1]:<34} {found[0][0] if found else 'ok'}")
for kind, line in found:
print(" " + line)
print(f"{len(tests)} tests: {len(tests) - len(problems)} passed, {len(problems)} failed")
class FakeMailer:
"""Stands in for the real mailer: keeps messages instead of sending them."""
def __init__(self):
self.sent = []
def send(self, to, subject, body):
self.sent.append((to, subject, body))
def send_low_stock_alerts(stock, mailer, reorder_point=10):
low = sorted(code for code, bags in stock.items() if bags < reorder_point)
if low:
mailer.send("theo@ridgeline.example", "Low stock", ", ".join(low))
return low
class TestAlerts(unittest.TestCase):
def test_low_coffees_are_emailed(self):
mailer = FakeMailer()
send_low_stock_alerts({"RC-101": 42, "RC-104": 9, "RC-105": 3}, mailer)
self.assertEqual(mailer.sent, [("theo@ridgeline.example", "Low stock", "RC-104, RC-105")])
def test_no_email_when_stock_is_fine(self):
mailer = FakeMailer()
send_low_stock_alerts({"RC-101": 42}, mailer)
self.assertEqual(mailer.sent, [])
report(TestAlerts)test_low_coffees_are_emailed ok test_no_email_when_stock_is_fine ok 2 tests: 2 passed, 0 failed
The address ends in .example, a domain reserved for documentation, so even a mistake could not send anything to a real person. The second test checks something that would be awkward to verify with real email: that no message is sent when nothing is low.
unittest.mock
Writing a fake class for every dependency gets tedious, so the standard library provides unittest.mock. Its Mock class creates objects that accept any method call or attribute access, return something sensible, and record everything that happened. You can tell a mock what to return, make it raise an exception, and afterwards ask it how it was called.
from unittest.mock import Mock
mailer = Mock()
mailer.send("theo@ridgeline.example", "Low stock", "RC-104")
print(mailer.send.called, mailer.send.call_count)
print(mailer.send.call_args)
mailer.send.assert_called_once_with("theo@ridgeline.example", "Low stock", "RC-104")
print("assert_called_once_with passed")
clock = Mock(return_value="2026-11-10") # a stub: always returns the same value
print(clock(), clock())
shop = Mock()
shop.fetch_orders.side_effect = ConnectionError("shop offline")
try:
shop.fetch_orders()
except ConnectionError as err:
print("raised:", err)True 1
call('theo@ridgeline.example', 'Low stock', 'RC-104')
assert_called_once_with passed
2026-11-10 2026-11-10
raised: shop offlineThree attributes do most of the work. return_value sets what calling the mock returns. side_effect, when set to an exception, makes the call raise it; when set to a list, makes successive calls return successive items, which is ideal for simulating a service that fails once and then succeeds. The assertion methods, such as assert_called_once_with, assert_called_with, assert_not_called and assert_any_call, check how the mock was used and raise AssertionError with a clear message if not.
import unittest
def report(*cases):
"""Run TestCase classes and print one line per test (a small test runner)."""
loader = unittest.TestLoader()
tests = [t for case in cases for t in loader.loadTestsFromTestCase(case)]
result = unittest.TestResult()
unittest.TestSuite(tests).run(result)
problems = {}
for kind, items in (("FAIL", result.failures), ("ERROR", result.errors)):
for test, tb in items:
problems.setdefault(test.id().split(" ")[0], []).append((kind, tb.strip().splitlines()[-1]))
for t in tests:
found = problems.get(t.id(), [])
print(f"{t.id().rsplit('.', 1)[-1]:<34} {found[0][0] if found else 'ok'}")
for kind, line in found:
print(" " + line)
print(f"{len(tests)} tests: {len(tests) - len(problems)} passed, {len(problems)} failed")
from unittest.mock import Mock
def import_orders(shop, attempts=2):
"""Fetch new online orders, trying again once if the connection drops."""
for attempt in range(attempts):
try:
return shop.fetch_orders()
except ConnectionError:
if attempt == attempts - 1:
raise
class TestImport(unittest.TestCase):
def test_retries_once_after_a_dropped_connection(self):
shop = Mock()
shop.fetch_orders.side_effect = [ConnectionError(), [{"id": 3101, "sku": "RC-102"}]]
self.assertEqual(import_orders(shop), [{"id": 3101, "sku": "RC-102"}])
self.assertEqual(shop.fetch_orders.call_count, 2)
def test_gives_up_after_two_failures(self):
shop = Mock()
shop.fetch_orders.side_effect = ConnectionError("offline")
with self.assertRaises(ConnectionError):
import_orders(shop)
self.assertEqual(shop.fetch_orders.call_count, 2)
report(TestImport)test_gives_up_after_two_failures ok test_retries_once_after_a_dropped_connection ok 2 tests: 2 passed, 0 failed
These tests check behavior that would be almost impossible to test with the real shop: what happens when the connection drops exactly once. With a mock, the test decides exactly when the failure occurs, and the tests run in microseconds.
Specs: mocks that know the real interface
A plain Mock accepts any attribute, which hides a dangerous kind of mistake. If the real mailer's method is called send but the code calls sned, a plain mock happily records a call to sned and the test passes, while the real program crashes. Giving the mock a specification fixes this. create_autospec(RealClass) builds a mock that only has the attributes the real class has, and checks that calls use the real method signatures.
from unittest.mock import Mock, create_autospec
class SmtpMailer:
def send(self, to, subject, body):
raise RuntimeError("would connect to a real mail server")
loose = Mock()
loose.sned("theo@ridgeline.example", "Low stock", "RC-104") # typo goes unnoticed
print("plain Mock accepted the typo")
strict = create_autospec(SmtpMailer, instance=True)
try:
strict.sned("theo@ridgeline.example", "Low stock", "RC-104")
except AttributeError as err:
print("autospec caught it:", err)
try:
strict.send("theo@ridgeline.example") # missing arguments
except TypeError as err:
print("autospec caught it:", err)plain Mock accepted the typo autospec caught it: Mock object has no attribute 'sned' autospec caught it: missing a required argument: 'subject'
As a rule, give mocks a spec whenever a real class exists. It costs one line and keeps the test honest about the interface it is pretending to use.
patch: replacing names during a test
Dependency injection is the cleanest approach, but sometimes code you cannot change reaches for a dependency by name, such as a module-level function or the random module. unittest.mock.patch temporarily replaces a name with a mock for the duration of a with block or a decorated test, and restores the original afterwards, even if the test fails. patch.object does the same for one attribute of a specific object or module.
import random
from unittest.mock import patch
COFFEES = ["RC-101", "RC-102", "RC-103", "RC-104", "RC-105", "RC-106"]
def coffee_of_the_week():
return random.choice(COFFEES)
with patch.object(random, "choice", return_value="RC-102") as fake_choice:
print(coffee_of_the_week())
fake_choice.assert_called_once_with(COFFEES)
print(random.choice is fake_choice) # restored after the with blockRC-102 False
The most common confusion with patch is where to patch. You must replace the name where the code under test looks it up, not where it was originally defined. If ridgeline/promo.py does from random import choice, then promo has its own name choice, and the test must patch ridgeline.promo.choice; patching random.choice would change nothing that promo sees. If promo instead does import random and calls random.choice, then patching random.choice works. This rule, patch where it is used, saves hours of puzzled debugging.
# tests/test_promo.py
from unittest.mock import patch
from ridgeline import promo
@patch("ridgeline.promo.choice", return_value="RC-102") # where promo looks it up
def test_coffee_of_the_week(fake_choice):
assert promo.coffee_of_the_week() == "RC-102"
# pytest's monkeypatch fixture does the same job:
def test_coffee_of_the_week_again(monkeypatch):
monkeypatch.setattr(promo, "choice", lambda items: "RC-102")
assert promo.coffee_of_the_week() == "RC-102"For randomness specifically, a simpler alternative to patching is often better: give the function a random.Random instance with a fixed seed, which produces the same sequence every time. That is dependency injection again, and it tests the real logic rather than a mock's canned answer.
Worked problem
Testing the daily alert at a fixed time
The alert should send only on weekdays, at the first run after 7:00. The function currently calls datetime.now() itself. Plan a testable design and the tests.
Show solution
- Change the signature to should_send(now) and pass datetime.now() from main.
- Test a Tuesday at 7:05: should_send returns True.
- Test a Tuesday at 6:59: returns False.
- Test a Saturday at 9:00: returns False.
- Test the boundary, Tuesday at exactly 7:00, after agreeing with Theo whether it counts.
Answer: Injecting now turns four hard-to-arrange situations into four one-line tests with fixed datetimes; no patching needed.
Do not mock everything
Mocks are powerful, and that makes them easy to overuse. A test that mocks every collaborator ends up checking that the code calls certain methods in a certain order, which is a description of the implementation rather than a check of behavior. Such tests break whenever the code is reorganized, even when it still works perfectly, and they can pass when the real pieces do not fit together at all, because the mocks only pretend.
Good guidelines are: mock only at the boundaries of your program, such as email, the network, the clock, randomness and payment terminals, and use the real objects for your own code; prefer a simple fake or an injected value over a mock with many expectations; check results and state where you can, and check calls only when the call itself is the behavior, as with sending an alert; and keep a few integration tests that use real pieces, so the doubles' assumptions are checked against reality now and then.
Try it yourself
Inject the date
days_until_sell_by(roast_date) uses date.today() inside, so its result changes every day. Add a parameter today=None that the function uses when given, and falls back to date.today() when not. Coffee should be sold within 30 days of roasting.
from datetime import date, timedelta
def days_until_sell_by(roast_date):
return (roast_date + timedelta(days=30) - date.today()).days
print(days_until_sell_by(date(2026, 11, 2)))Try it yourself
Check the alert with a Mock
Write send_low_stock_alerts(stock, mailer). It finds the coffee codes with fewer than 10 bags, sorted, and if there are any it calls mailer.send("theo@ridgeline.example", "Low stock", text) exactly once, where text is the codes joined by ", ". If nothing is low it must not call send. It returns the list of low codes.
The checks pass in a unittest.mock.Mock as the mailer and inspect how it was called.
def send_low_stock_alerts(stock, mailer):
low = [code for code, bags in stock.items() if bags < 10]
for code in low:
mailer.send('theo@ridgeline.example', 'Low stock', code)
return lowTry it yourself
Retry once with side_effect
Write import_orders(shop) that returns shop.fetch_orders(). If that call raises ConnectionError, it should try exactly one more time; if the second call also fails, the ConnectionError should propagate. The checks use a Mock whose side_effect decides when the connection drops.
def import_orders(shop):
return shop.fetch_orders()Common mistakes
MistakeCalling date.today(), datetime.now() or random functions deep inside the code under test.
InsteadPass the date, clock or random generator in as an argument so tests can control it.
MistakePatching the name where it is defined, such as random.choice, when the code did from random import choice.
InsteadPatch where the name is looked up: the module that uses it.
MistakeUsing a plain Mock for a class that exists, so typos in method names pass silently.
InsteadUse create_autospec or Mock(spec=RealClass).
MistakeMocking your own small classes and checking every internal call.
InsteadUse real objects for your own code and mock only external boundaries.
MistakeLetting a test send real email, call the real shop or touch the real data file.
InsteadReplace those boundaries with fakes, stubs or mocks in every unit test.
MistakeAssuming mocks prove the real pieces fit together.
InsteadKeep a few integration tests that use the real collaborators.
Summary and key terms
- Test doubles replace slow, unpredictable or external dependencies, such as email, clocks, randomness and web services, during tests.
- Dependency injection, passing dependencies in as arguments, makes code easy to test without tricks.
- Dummies, stubs, fakes, spies and mocks differ in whether they return canned values, really work, or record and check calls.
- unittest.mock.Mock records calls; return_value and side_effect control results; assert_called_once_with and friends check usage.
- create_autospec makes mocks that match the real interface and catch typos and wrong arguments.
- patch and patch.object replace names temporarily; patch where the name is looked up.
- Mock only at the boundaries; prefer real objects, fakes and injected values, and keep some integration tests.
- test double
- Any object that stands in for a real dependency during a test.
- dependency injection
- Passing an object's dependencies in from outside instead of creating or fetching them inside.
- stub
- A test double that returns canned answers.
- fake
- A test double that is a simple working implementation, such as an in-memory store.
- spy
- A test double that records how it was used for later checking.
- mock
- A test double that records calls and can be checked against expectations.
- side_effect
- A Mock setting that makes calls raise an exception or return successive values.
- autospec
- Creating a mock from a real class so it has only that class's attributes and signatures.
- patch
- A unittest.mock tool that temporarily replaces a name with a mock.
Practice problems
Work each problem on paper first, then reveal the answer to check.
Why is label(roast_date) that calls date.today() hard to test?
Show answer
Its result depends on the day the test runs, so no fixed expected value can be checked.
Which double would you use to give the code a clock that always reads 10 November 2026?
Show answer
A stub, such as Mock(return_value=...) or a simple function returning that date.
What does shop.fetch_orders.side_effect = [ConnectionError(), [1]] make the mock do?
Show answer
The first call raises ConnectionError and the second returns [1].
promo.py does from random import choice. Which name should a test patch?
Show answer
ridgeline.promo.choice, where promo looks the name up.
Why does create_autospec catch a call to mailer.sned?
Show answer
The spec only has the real class's attributes, so an unknown attribute raises AttributeError.
Give one sign that a test is over-mocked.
Show answer
It breaks when code is reorganized without any change in behavior, because it checks internal calls.
Lesson quiz · 5 questions
Score 60% or more to complete this lesson.
Create a free account to save your progress, take quizzes and earn a certificate.