Pebellslearn by building
0%
RoadmapsMatrixCoursesSandboxSign in
Testing & Debugging  ›  Lesson

Doubles, patch & Test Seams

Mocking 26 minReplace the network, the clock and other edges with doubles you control, and patch names where they are looked up
You're building a piece ofQuarry — the test rig
This piece — fake_source(): Stands in for the network, replays canned answers and timeouts, and remembers every call.
Scenario Quarry's fetch tests failed every time the stock server hiccuped; the rig now answers for the network, timeouts included.
Your task
Build fake_source(canned, names): write a FakeSource whose get(name) takes the next response from canned[name] (repeating the last one once only one remains), raises TimeoutError for "!timeout", raises FileNotFoundError for unknown names and records every call. Run Quarry's fetch_all with it and return {"results": ..., "calls": ...}. Example: fake_source({"orders": ["!timeout", "id,total"]}, ["orders"]) → {"results": {"orders": "id,total"}, "calls": ["orders", "orders"]}.

Doubles, patch & Test Seams

In short: a test double behaves like a real dependency but is completely under the test's control — a seam is where you plug one in, and the easiest seam is a parameter. unittest.mock.Mock invents any attribute or call you touch, remembers everything (call_count, call_args_list), and answers with return_value or a scripted side_effect. When there's no parameter to pass a double through, patch("dotted.name") swaps that name for a double for the length of a with block — but a module's names live in its own dictionary, so the rule every guide repeats is patch the name where it is looked up, not where it is defined.

patch("clock.now") rebinds only clock's own dict entry; report copied its own reference when it did "from clock import now", so report.stamp() still sees the real function until report.now itself is patched

Quarry's fetch stage pulls exports from remote sources, retries the ones that time out, and stamps every report with the current time. Test it directly and your suite now depends on the network being up, the server being in a good mood, and the clock reading whatever it reads today — slow, flaky, and it can never check the timestamp because it's different every run. The fix: hand the code something that behaves like the network or the clock but is entirely under the test's control.

Replace the network with a fake you control

Here's Quarry's fetch loop. Notice what it does not do: it never opens a connection itself. It's handed a source and calls source.get(name):

python

That source parameter is the seam. In production Quarry passes a real HTTP source; in a test it passes a fake — no network, no waiting, and the fake can produce a timeout on demand, the one thing a real server will never do on request. Designing for a seam is most of the work of testing; code that builds its own connection or reads the clock directly can still be tested, but only by the heavier tool below.

People say "mock" for five different things, and the differences tell you what a test is really checking: a dummy fills a parameter and is never used; a stub returns canned answers; a fake is a working, simplified implementation (FakeSource above); a spy also records how it was called (FakeSource.calls); a mock is a spy you make assertions about the calls on. Prefer checking the result when you can; check interactions only for behaviour with no other visible trace, like a retry.

Freeze the clock with unittest.mock

Writing a fake class for every collaborator gets tedious. Mock is an object that accepts any attribute and any call, remembers all of them, and returns whatever you configure:

python

source.get didn't exist until you touched it. return_value is the stub half; call_count is the spy half; assert_called_with is the mock half.

What does this print?
from unittest.mock import Mock

fetch = Mock(return_value="ok")
fetch("orders")
fetch("stock")
print(fetch.call_count)

To make a double raise, or answer differently each call, use side_effect — a list is played back one item per call, and once it's exhausted the mock raises StopIteration:

What happens on the third call?
from unittest.mock import Mock

fetch = Mock(side_effect=["a", "b"])
fetch()
fetch()
fetch()

The trap: a plain Mock invents any attribute you touch, so code calling a method that doesn't exist on the real class still works against it. spec ties the double to the real interface, and misspellings fail loudly:

python

A test that passes against a loose mock and fails in production is the classic over-mocking bug — use spec whenever a double stands in for a real class.

Patch the name the code actually looks up

Sometimes there's no seam. Quarry's report module reads the clock itself:

# quarry/report.py
from quarry.clock import now

def stamp():
    return "report @ " + now()

patch swaps a name for a double for the length of a with block, then restores it. The hard part is choosing which name. A module's names live in a dictionary, its __dict__. from quarry.clock import now does not create a link to clock.now — it looks up the object clock.now currently points at and copies that reference into report's own dictionary, under report's own name now. From then on, the two modules hold two separate names that happen to point at the same function.

step through it
1clock = {"now": "real_now"}
2report = {"now": clock["now"]} # what `from clock import now` does
3 
4saved = clock["now"]
5clock["now"] = "fake_now" # patch("clock.now") rebinds clock's name
6seen_by_report = report["now"] # report still has its own copy
7clock["now"] = saved # patch restores it on exit
8 
9saved = report["now"]
10report["now"] = "fake_now" # patch("report.now"): the name report uses
11seen_after_fix = report["now"]
12report["now"] = saved
13print(seen_by_report, seen_after_fix)
Drawing diagram…

Hence the rule: patch the name where it is looked up, not where it is defined. Here it is for real, with two in-memory modules:

broken — fix it

The test patches the clock, yet the report still prints the real time. Change only the patch target so the report sees the fake.

patch.dict(d, {...}) temporarily changes entries in any dictionary — including globals(), which patches a name in the code you're running right now:

python

The second line proves the restore. (pytest's monkeypatch fixture does the same job and undoes it automatically when the test ends.)

Quick practice

What does this print?
from unittest.mock import Mock

calls = []
def slow_sum(a, b):
    calls.append((a, b))
    return a + b

fake_sum = Mock(side_effect=slow_sum)
print(fake_sum(2, 3), fake_sum.call_count, calls)
What does this print?
from unittest.mock import Mock

class Vault:
    def save(self, row):
        pass

double = Mock(spec=Vault)
try:
    double.saev("x")
except AttributeError:
    outcome = "rejected"
double.save("x")
print(outcome, double.save.call_count)

Doubles matter in one number: if each real network call fails with probability f, a suite making x real calls goes red for no reason with probability 1 − (1 − f)ˣ. At 200 calls, a service failing one call in a thousand turns the suite red 18% of the time — training everyone to ignore red. And don't mock the thing you're testing: a test that patches parse_line to test parse_line is testing the mock, not the code.


🎯 Your turn

Build fake_source(canned, names). Write a FakeSource whose get(name) answers from canned, a dict of lists of responses: each call to a name takes the next response from its list, and when only one is left it keeps answering with that one (a server that stays down stays down). The response "!timeout" means raise TimeoutError; a name not in canned raises FileNotFoundError. Record every name asked for in calls.

Then drive Quarry's real fetch_all (it's in the starter) with it and return {"results": ..., "calls": ...}:

fake_source({"orders": ["!timeout", "id,total"]}, ["orders"])
# → {"results": {"orders": "id,total"}, "calls": ["orders", "orders"]}

Practice — level up

0/6 solved

Solve each in its own editor — Run to try, Check to grade. Stuck? Reveal a Hint, or the full Solution + walkthrough.

Warm-upA spy around max

A spy is a wrapper that records its calls and then does the real work. Write spy_calls(args_list): wrap the built-in max in a spy, call it once for each list of arguments in args_list (unpacked), and return {"calls": [...], "results": [...]} — each call's arguments as a list, and what each returned.

mockingfirst-class-functions
DrillPlay back a side_effect list

Write side_effects(effects, n). Build a unittest.mock.Mock whose side_effect is effects, except that any string starting with ! becomes an instance of the built-in exception with that name ("!TimeoutError" → TimeoutError()). Call the mock n times and return a list with, per call, the value it returned or "raised <Name>".

mockingexceptionsbuiltins
BuildFreeze the clock

report() reads the clock through the module-level name now. Write stamp_report(fixed) that uses unittest.mock.patch.dict on globals() to make now return fixed while it calls report(), then calls report() again after the patch has ended. Return {"inside": ..., "after": ...}.

mockingpatchdictionaries
BossDrive the retry with a mock

with_retry(fetch, attempts) is Quarry code: it calls fetch() up to attempts times, retrying on TimeoutError. Test it: write probe_retry(responses, attempts) that builds a Mock whose side_effect plays back responses (the string "!timeout" means a TimeoutError), runs with_retry with it, and returns {"result": ..., "calls": ...} — the value returned, or "gave up" if with_retry raised, plus the mock's call_count.

mockingside-effectexceptions
CapstoneA fake world: source and clock together

run_batch(source, clock, names) is Quarry code that reads each source, counts rows and times itself. Write FakeClock (starts at 0, now() returns the current second, advance(s) moves it on) and FakeSource(pages, clock), whose get(name) records the name in calls, then advances the clock by 30 and raises TimeoutError if the page is "!timeout", otherwise advances by 1 and returns the page. Write nightly_run(pages, names) returning run_batch's dict plus a "calls" key.

mockingclassestest-design
ExplainPatch where it is looked up

report.py does from clock import now, and a test uses patch("clock.now") — but the report still shows the real time. Explain why, and what the test should patch instead.

fake_source.py
Call the function you wrote — just like your app's frontend would. Edit the inputs and hit Call.
fake_source(canned, names) → dictDrive fetch_all with a fake source and report what it returned and what it asked for.
try:
returned
Press Run to execute your code.
Press Check to run the tests for this lesson.