Test Dependencies with unittest.mock
Write a function with an injected dependency and test its return value and interaction without external I/O.
Example check: Worked and guided code samples are checked against CPython 3.13. Practice uses Pyodide 0.29.5 (Python 3.13) in your browser. How we check lessons
Video lesson: Test Dependencies with unittest.mock
The preview is stored on this site. YouTube loads only when you press Play. Watch on YouTube
Jump to a chapter
Read the video transcript
Select a timestamp to watch that moment on YouTube.
- 00:00
A function can return the right text while doing the wrong work. Imagine a user label function that asks a service for user seven. A test with a fixed service response may show “7: Ada” and pass, even if the function requested the user twice. In a real system, that duplicate request can waste time, consume an API quota, or produce inconsistent data if the service changes between calls. We will make that error visible with `unittest.mock.Mock`. A mock can supply a controlled result and record how it was called. The function itself should not require a Mock; it should accept any callable that follows the fetcher contract.
- 00:42
That is dependency injection: pass the external operation into the function rather than reaching out to a live service from inside the code we want to test. We will run the broken version, inspect its call count, write a corrected function, and test both a valid result and invalid response data. Everything runs without a network connection. The visible label looks perfect. The problem is in the interaction: the condition calls the fetcher once, and the return expression calls it again. Our Mock always returns the same dictionary, so the mistake does not affect this output. A live service could answer differently on the second call, or fail.
- 01:24
This is why the contract should include the interaction when that interaction matters. We want one fetch using exactly the ID supplied to the function. A test that checks only `result == '7: Ada'` cannot prove that. We can ask the Mock for its call count or use its stronger `assert_called_once_with(7)` method. Before changing code, predict how that assertion will behave for this broken function. It must fail because there were two calls. This is a useful debugging moment: mocks are not simply fake return-value machines. They can show what a piece of code asked its dependency to do. But we should still avoid checking irrelevant internal details; the one-call rule is part of this function's explicit contract.
- 02:11
The assertion fails for exactly the reason we expected: there are two calls, both with user ID seven. The recorded list makes that visible without requiring a network trace. The repair is straightforward: call the dependency once, store its answer in a local variable, validate it, and format the result. The validation step matters because mocks make it easy to provide a perfect dictionary, while a real boundary might return missing or malformed data. Our contract says the result is a dictionary with a nonblank string name. If the name is missing, None, or whitespace, the function should raise ValueError.
- 02:51
It should not produce the literal text “None” as a user name. Also remember what this test cannot prove: it cannot verify that a real service is reachable or that its schema never changes. It verifies the logic on our side of the boundary and the request shape we promised. Integration tests can separately check a real service when those are warranted. We call fetch_user exactly once and keep the result. The dictionary check makes the expected response type explicit. The name check separates a real string from missing data and rejects strings that are only whitespace. After that, formatting is safe and the label contains the trimmed name.
- 03:33
The Mock verifies the one-call rule and exact argument. Notice that the function does not import Mock; only the demonstration test does. In production, the caller can pass a function that talks to a database or an HTTP API. In the browser exercise, a fake keeps the result deterministic and offline. This is an important design benefit beyond testing: the function describes what it needs through an argument rather than hard-coding where data comes from. Do not overuse mocks, though. A simple hand-written fake can be easier to read when all you need is one fixed response. And if a mock accepts any attribute or argument, it may hide an interface mistake; for larger interfaces, use `spec` or `autospec` and keep some integration coverage.
- 04:24
A different user ID verifies that we did not hard-code seven. The Mock confirms exactly one call with forty-two. The lambda for user three is an ordinary callable, showing that the production function depends on behavior rather than the test library. Three malformed responses then raise ValueError: an empty dictionary, a blank name, and no dictionary at all. These cases protect the boundary where data enters our logic. They do not claim that every real API response is covered, and they do not replace a separate integration test. Open the matching PythonLessonLab lesson and complete the browser task.
- 05:04
If your result is correct but a hidden check fails, inspect the call count and the ID sent to the fetcher. If an invalid-data check fails, ask whether you validated before formatting. The broader lesson is to make external dependencies explicit, substitute a controlled implementation in unit tests, and verify the interactions that form part of the contract. A mock helps you see a hidden request; careful function design makes that request easy to replace in the first place.
Understand the concept
A unit test should explain a function's behavior without relying on a live service. If a function calls a real API directly, a slow connection or changing response can make a simple test fail for the wrong reason. Dependency injection solves that problem by passing the operation in as an argument. In this lesson, the function receives fetch_user, a callable that accepts a user ID and returns a small dictionary. Production code can pass a real fetcher; the browser exercise passes a controlled fake.
unittest.mock.Mock can stand in for that callable. `Mock(return_value={'name': 'Ada'})` records calls and returns the chosen dictionary. After invoking the function, `assert_called_once_with(7)` checks the interaction as well as the returned label. An assertion on output alone would not reveal a function that fetched the wrong ID or fetched twice. At the same time, tests should not assert every internal step; verify interactions only when they are part of the contract.
Mock does not make an unsafe design safe. A broad mock with no interface can accept arguments a real dependency would reject. A small hand-written fake is sometimes clearer, and `spec` or `autospec` can catch some interface mistakes in larger tests. Our exercise keeps the contract explicit: the fetcher returns a mapping with a nonblank string name, and load_user_label returns a compact label. Invalid data raises ValueError rather than silently producing a misleading label.
The checked exercise implements load_user_label(user_id, fetch_user). Its prepared run uses a Mock, prints the result, and verifies one call. Hidden checks use another Mock and a simple callable to ensure the function does not depend on Mock itself. This distinction matters: we mock the boundary in tests, while the function works with any callable obeying the same contract.
- Inject the external operation as a callable
- Mock controls the returned value and records calls
- Assert behavior and only contractual interactions
- A mock should model the real dependency's interface
See it step by step
Read the code, predict the output, then compare it with the result.
01. Control a dependency's answer
from unittest.mock import Mock
def display_count(read_count):
return f"Items: {read_count()}"
fake = Mock(return_value=4)
print(display_count(fake))
fake.assert_called_once_with()Items: 4The function sees a callable; the test chooses its response and checks that it was used once.
02. Check arguments as well as the result
from unittest.mock import Mock
def lookup(code, fetch):
return fetch(code).upper()
fake = Mock(return_value="ready")
print(lookup("A7", fake))
fake.assert_called_once_with("A7")READYThe call assertion would expose a wrong or repeated lookup even if the visible result happened to match.
03. Use a tiny fake when it communicates more clearly
calls = []
def fake_price(sku):
calls.append(sku)
return 12
def price_label(sku, fetch_price):
return f"{sku}: $" + str(fetch_price(sku))
print(price_label("P1", fake_price))
print(calls)P1: $12
['P1']A hand-written fake can record one simple interaction without any mocking machinery.
A closer look
Follow the reasoning, inspect each result, then try the suggested changes in the console below.
Expose the external call as an input
A function that reaches into a live service on its own is difficult to test in isolation. Its test can fail because the network is down, credentials expired, or service data changed. Passing a fetch_user callable makes the boundary visible. The function promises to call it once with the requested ID, then format a validated result.
This example uses a hand-written fake first. It records calls in a list and returns a known dictionary. Nothing contacts a server. The observable label and the recorded ID are both part of the test. Keep the fake small enough that it cannot become a second implementation of the production service.
calls = []
def fake_fetch(user_id):
calls.append(user_id)
return {"name": "Ada"}
def load_user_label(user_id, fetch_user):
user = fetch_user(user_id)
return f"{user_id}: {user['name']}"
print(load_user_label(7, fake_fetch))
print(calls)7: Ada
[7]- Pass the fake into the function instead of importing a real network client.
- Check that the requested ID is the one actually sent to the dependency.
Use Mock to detect a duplicate request
A Mock can return a fixed result and remember every call. That makes it helpful when a function accidentally fetches twice. If the mock always returns the same dictionary, checking only the final label would miss the extra call. `assert_called_once_with` checks both the count and the argument, expressing the dependency contract directly.
In the broken example, the first fetch appears in the condition and a second fetch appears in the return expression. The result looks correct, but the assertion reports a problem. Replace those two calls with one local variable. In real software, duplicate calls can waste time, increase API costs, or even see two different states.
from unittest.mock import Mock
def broken_label(user_id, fetch_user):
if fetch_user(user_id):
return f"{user_id}: {fetch_user(user_id)['name']}"
return "missing"
fake = Mock(return_value={"name": "Ada"})
print(broken_label(7, fake))
print(fake.call_count)7: Ada
2- Count the two calls hidden behind a correct-looking label.
- Use an interaction assertion to expose the duplication that output checking misses.
Validate the fake response and verify one call
A controlled fake is only as useful as the contract it represents. The fetcher is expected to return a dictionary with a nonblank string name. Validate those assumptions before formatting. A missing name should raise ValueError, not produce a label containing None or whitespace. The function still knows nothing about Mock; any callable with the promised response shape works.
After running the corrected function, verify the output and call with assert_called_once_with. Add a second input that returns an invalid dictionary. That test proves the function handles malformed boundary data rather than only the happy path. Larger test suites may use spec or autospec to constrain a mock's interface, but a focused callable contract is enough here.
from unittest.mock import Mock
def load_user_label(user_id, fetch_user):
user = fetch_user(user_id)
if not isinstance(user, dict):
raise ValueError("invalid user")
name = user.get("name")
if not isinstance(name, str) or not name.strip():
raise ValueError("name required")
return f"{user_id}: {name.strip()}"
fake = Mock(return_value={"name": " Ada "})
print(load_user_label(7, fake))
fake.assert_called_once_with(7)
try:
load_user_label(8, lambda user_id: {})
except ValueError as error:
print(error)7: Ada
name required- Store the single response and validate its name before formatting.
- Use Mock for the interaction check and an ordinary callable for the invalid-data case.
Try it in Python
Edit the example and run it. Python starts in your browser the first time you click Run.
Python console
Ready to runNeed input()? Add one value per line
Your output appears here.
Need a hint?
Store the result of `fetch_user(user_id)` once. Check `isinstance(user, dict)`, then `isinstance(user.get('name'), str)` and `user['name'].strip()`. Raise ValueError before formatting an invalid result.
Complete the task and select Check task to verify your code.
Quick quiz
Three questions. You can change your answers and try again.
Typical mistakes
Everyone meets these errors. See what causes them and how to fix them.
Fetching twice accidentally
if fetch_user(user_id):
return fetch_user(user_id)['name']user = fetch_user(user_id)
return user['name']What happens: The dependency is called twice and could return different data or incur two requests.
Store the single response before inspecting it.
Checking output but ignoring a wrong argument
assert load_user_label(7, fake) == '7: Ada'assert load_user_label(7, fake) == '7: Ada'
fake.assert_called_once_with(7)What happens: A function could call the fake with a different ID and still receive its fixed return value.
Verify a contractual interaction when a fixed mock response would hide a wrong call.
Accepting missing data as a real name
return f"{user_id}: {user.get('name')}"name = user.get('name')
if not isinstance(name, str) or not name.strip(): raise ValueError('name required')What happens: A missing name becomes the literal text None.
Reject invalid boundary data explicitly.