PYTHON / TESTING AND TOOLING
Mocking and dependency boundaries
Isolate code from networks, clocks and filesystems by injecting fakes or patching the name where it is looked up, using autospec to keep doubles honest.
What you will learn
- Patch the name in the module under test, not where the function was defined
- Prefer passing a dependency in as an argument over patching a module global
- Use create_autospec or autospec=True so wrong call signatures fail the test
- Assert on recorded arguments with assert_called_once_with, not just call_count
Understanding Mocking and dependency boundaries
A dependency boundary is the point where your function stops computing and starts talking to something you do not control: a socket, the system clock, os.environ, a database driver, subprocess. Those calls are what make tests slow, order-dependent, or dependent on someone else's uptime. Mocking replaces the object on your side of that boundary with a recorder: the test decides what the dependency returns, and afterwards inspects what was asked of it. The useful mental model is that the boundary is an input to your logic, not a hidden global your logic reaches for.
unittest.mock.patch operates on names, not on objects. When orders.py does `from rates import fetch_rate`, Python copies that reference into orders' own globals, so rebinding rates.fetch_rate later leaves orders untouched; you must patch "orders.fetch_rate". This is why the rule is 'patch where it is looked up'. patch also restores the original binding when its context manager exits or its decorated function returns, which is what keeps one test from leaking a double into the next.
A bare Mock() is deliberately permissive: any attribute you touch springs into existence as a child mock, and any call returns another mock. That is convenient and dangerous, because a test can pass against a method name that does not exist or a signature that real code would reject. create_autospec and patch(..., autospec=True) build the double from the real object, so those errors surface in the test instead of in production. Treat heavy patching as a design signal: if a test needs four patches, the code is reaching across too many boundaries and should take them as parameters.
from unittest.mock import Mock, patch
def fetch_rate(currency):
raise RuntimeError("real network call")
def total_in_usd(amounts, currency):
return round(sum(amounts) * fetch_rate(currency), 2)
fake = Mock(return_value=1.25)
with patch(f"{__name__}.fetch_rate", fake):
print(total_in_usd([10.0, 22.0], "EUR"))
print(fake.call_count)
print(fake.call_args)
try:
total_in_usd([10.0, 22.0], "EUR")
except RuntimeError as exc:
print("after patch:", exc)
Replace the dependency at the boundary your code looks it up in, and constrain the replacement to the real object's shape.
Worked examples
Autospec rejects calls the real object would reject
Shows how a plain Mock swallows a misspelled method and a wrong signature, while an autospec double refuses both.
from unittest.mock import Mock, create_autospec
class Mailer:
def send(self, to, subject):
raise RuntimeError("real SMTP")
def notify(mailer, user):
mailer.send(user["email"], subject="Welcome")
loose = Mock()
print(type(loose.sned("nobody")).__name__)
strict = create_autospec(Mailer, instance=True)
notify(strict, {"email": "a@example.com"})
strict.send.assert_called_once_with("a@example.com", subject="Welcome")
print("assertion passed")
try:
strict.sned("nobody")
except AttributeError:
print("blocked: AttributeError")
try:
strict.send()
except TypeError:
print("blocked: TypeError")
Example explained
Line 1loose.sned("nobody") invents an attribute and returns a Mock, so a typo in the test never fails.
Line 2create_autospec(Mailer, instance=True) copies Mailer's attributes and signatures onto the double.
Line 3assert_called_once_with checks the exact positional and keyword arguments, including that subject was passed by keyword.
Line 4strict.send() raises TypeError because the recorded signature still requires to and subject.
A seam that needs no patching
Passing the clock in as an object turns a hidden dependency into an ordinary argument, so a hand-written stub replaces it.
class FrozenClock:
def __init__(self, readings):
self._readings = list(readings)
def now(self):
return self._readings.pop(0)
def timed(clock, work):
start = clock.now()
result = work()
return result, clock.now() - start
calls = []
value, seconds = timed(FrozenClock([100.0, 102.5]), lambda: calls.append("ran"))
print(seconds)
print(calls)
Example explained
Line 1timed never names time.time, so there is no global to patch and no import path to get wrong.
Line 2FrozenClock pops a fixed reading per call, making the elapsed value deterministic instead of near zero.
Line 3The stub is eight lines of ordinary Python, so failures point at your code rather than at mock configuration.
Important notes
In the main example f"{__name__}.fetch_rate" resolves to "__main__.fetch_rate" because the file runs as a script; inside a package the target is the real import path, such as "shop.orders.fetch_rate".
Avoid mocking third-party client objects directly: your mock encodes your guess about their API. Wrap them in a thin adapter you own and mock that instead.
Common mistakes
Patching "rates.fetch_rate" when the module under test did `from rates import fetch_rate` — the module's own binding is untouched, so the real network call still runs and the test hangs or fails intermittently.
Using Mock() with no spec: a call to client.get_user() on a class that only has fetch_user() returns a happy mock, the test passes, and production raises AttributeError.
Misspelling an assertion, such as mock.assert_called_once_wth(3), which mock treats as a new child attribute and silently returns — the test asserts nothing at all.
Try it yourself
Change, predict, then run
Write a function report(fetch_users) that returns the count of active users, then test it twice: once with a lambda returning a fixed list, and once with create_autospec so an accidental extra argument in your call fails the test.
Open the Python workspaceCheck your understanding
app.py contains `from services import fetch_rate` and calls fetch_rate(). A test does `with patch("services.fetch_rate", Mock(return_value=1.0)):` but the real network function still runs. What explains this?
- app.py holds its own reference to the function in its globals, so rebinding the services attribute does not change what app.py looks up
- patch() can only replace methods on classes, not plain module-level functions
- The patch was applied after app.py was imported, and patch cannot affect modules that are already in sys.modules
- fetch_rate must be wrapped or decorated before mock is able to replace it
Show answer
The from-import copied the function object into app.__dict__ at import time; patch replaced services.fetch_rate, a different binding to the same object, so app.py's lookup is unaffected — patch("app.fetch_rate") fixes it. Option 3 is tempting because timing sounds relevant, but patch always runs after imports and works precisely by mutating already-imported modules; what matters is which namespace holds the name being looked up.