Python Basics: Replacing Dependencies with unittest.mock
A test should be able to check a retry without waiting for a real timeout. Python’s standard-library unittest.mock module lets you control a dependency’s responses and inspect how your code called it.
Before you start
These three independent scripts were tested on Linux with Bash, Python 3.12.14 and Python 3.13.5. They require no extra packages or environment variables. Commands use python3; substitute python3.13 for the other tested interpreter. Run without -O, which disables ordinary assert statements.
Create a fresh working directory and keep this Bash session open:
start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 1Save each UTF-8 source block in this directory using its stated filename. Avoid unittest.py, which would shadow the standard-library package. The examples send no messages, make no network requests, and create no data files. They run directly, without a test runner.
Check a dependency’s arguments
Save as demo_mock_notification.py:
from unittest.mock import create_autospec
def send_notice(address, message):
raise RuntimeError("No real sender is configured")
def announce(address, sender):
return sender(address, "Build finished")
sender = create_autospec(send_notice, return_value="queued")
result = announce("reader@example.com", sender)
assert result == "queued"
sender.assert_called_once_with("reader@example.com", "Build finished")
print("Result:", result)
print("Recipient and message checked")
try:
sender("reader@example.com") # Intentional missing argument.
except TypeError:
print("Expected error: message argument is missing")
else:
raise AssertionError("The incomplete call should fail")
print("Recorded valid calls:", sender.call_count)Run it:
python3 demo_mock_notification.pyOutput:
Result: queued
Recipient and message checked
Expected error: message argument is missing
Recorded valid calls: 1create_autospec() copies a callable’s signature. The replacement returns our chosen value without running send_notice. assert_called_once_with() verifies both the call count and arguments. The incomplete call fails before being recorded. Autospeccing checks the call shape, not argument types. See auto-speccing.
Passing sender into announce makes the dependency easy to replace. The placeholder raises if accidentally used, so this demonstration cannot silently send anything.
Exercise a retry path
Save as demo_mock_retry.py:
from unittest.mock import Mock, call
def read_status(fetch):
try:
return fetch("job-7")
except TimeoutError:
return fetch("job-7")
fetch = Mock(side_effect=[TimeoutError("busy"), "ready"])
status = read_status(fetch)
assert status == "ready"
assert fetch.call_args_list == [call("job-7"), call("job-7")]
print("Recovered status:", status)
print("Attempts:", fetch.call_count)
always_fails = Mock(side_effect=TimeoutError("still busy"))
try:
read_status(always_fails)
except TimeoutError as error:
print("Expected final failure:", error)
else:
raise AssertionError("Two timeouts should propagate the second error")
assert always_fails.call_count == 2
print("Failed attempts:", always_fails.call_count)
try:
fetch("job-7") # Intentional call beyond the two configured responses.
except StopIteration:
print("Expected error: response sequence is exhausted")
else:
raise AssertionError("The response sequence should be exhausted")Run it:
python3 demo_mock_retry.pyOutput:
Recovered status: ready
Attempts: 2
Expected final failure: still busy
Failed attempts: 2
Expected error: response sequence is exhaustedAn iterable side_effect supplies successive results or exceptions; exhaustion raises StopIteration. A single exception instance raises on every call. Comparing call_args_list with the expected list checks the complete sequence. See side effects and return values.
Our helper allows one retry. The second case checks that it eventually gives up. The final call illustrates a finite mock setup, not a third application retry. No timeout actually elapses.
Temporarily replace an imported clock
Save as demo_mock_clock.py:
from time import time
from unittest.mock import patch
def event_label():
return f"created:{int(time())}"
original_clock = time
with patch(f"{__name__}.time", return_value=1_700_000_000) as clock:
label = event_label()
assert label == "created:1700000000"
clock.assert_called_once_with()
print("Event:", label)
assert time is original_clock
print("Original clock restored:", time is original_clock)Run it:
python3 demo_mock_clock.pyOutput:
Event: created:1700000000
Original clock restored: TruePatch the name the tested function looks up. Here from time import time creates a local module name, so patching time.time would miss it. With direct script execution, __name__ is "__main__". The context manager restores the original object on exit, including exceptional exits. See where to patch.
The chosen timestamp keeps the output stable. This checks label formatting; it does not test the system clock. Similarly, a mocked sender cannot prove that a real delivery service works. Keep separate checks for important real integrations.
Clean up
In the same Bash session, remove the scripts and empty working directory:
rm -- demo_mock_notification.py demo_mock_retry.py demo_mock_clock.py
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dirReferences
Acknowledgements
This post was created with assistance from AI.