Python Mocking and Test Doubles Practice Problems & Exercises
Practice: Mocking and Test Doubles
← Back to lessonEasy
Task: Create a MagicMock, configure its return_value to return a dict with a "name" key, call it, then print the name, .called, and .call_count.
Solution:
from unittest.mock import MagicMock
fetch_user = MagicMock()
fetch_user.return_value = {"name": "Alice"}
result = fetch_user()
print(result["name"]) # Alice
print(fetch_user.called) # True
print(fetch_user.call_count)# 1
from unittest.mock import MagicMock
# Create a mock for a user-fetching function
fetch_user = MagicMock()
# Configure it to return {"name": "Alice"} when called
# Call it once
# Print the "name" from the result
# Print whether it was called (True/False)
# Print how many times it was called
Expected Output
Alice\nTrue\n1Hints
Hint 1: Create a mock with `mock = MagicMock()`. It accepts any attribute access or call.
Hint 2: Set `mock.return_value = value` to control what it returns when called.
Hint 3: Use `mock.called` to check if it was called and `mock.call_count` for the number of times.
Task: Add the @patch("requests.get") decorator to test_fetch_status. Configure the mock's return_value to simulate a 200 response with text="ok". Assert both return values.
Solution:
from unittest.mock import patch, MagicMock
import unittest
import requests
def fetch_status(url):
resp = requests.get(url)
return resp.status_code, resp.text
class TestFetch(unittest.TestCase):
@patch("requests.get")
def test_fetch_status(self, mock_get):
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.text = "ok"
mock_get.return_value = mock_response
status, body = fetch_status("https://example.com")
self.assertEqual(status, 200)
self.assertEqual(body, "ok")
print(f"Status: {status}")
print(f"Body: {body}")
if __name__ == '__main__':
unittest.main()
from unittest.mock import patch, MagicMock
import unittest
# Imagine this lives in myapp.py:
# import requests
# def fetch_status(url):
# resp = requests.get(url)
# return resp.status_code, resp.text
import requests
def fetch_status(url):
resp = requests.get(url)
return resp.status_code, resp.text
class TestFetch(unittest.TestCase):
# patch requests.get so no real HTTP call is made
def test_fetch_status(self, mock_get):
# configure mock_get.return_value with status_code=200, text="ok"
# call fetch_status with any URL
# assert status == 200 and body == "ok"
pass
if __name__ == '__main__':
unittest.main()
Expected Output
Status: 200\nBody: okHints
Hint 1: `@patch("module.ClassName")` replaces the target with a `MagicMock` for the duration of the test.
Hint 2: The mock is passed as the last argument to your test function.
Hint 3: Always patch at the location where the name is used, not where it is defined.
Task: After calling send_email, use assert_called_once_with on mailer.send to verify the exact keyword arguments.
Solution:
from unittest.mock import MagicMock
def send_email(mailer, to, subject, body):
mailer.send(to=to, subject=subject, body=body)
mailer = MagicMock()
mailer.send.assert_called_once_with(
subject="Hello",
body="Welcome!"
)
print("All assertions passed")
from unittest.mock import MagicMock
def send_email(mailer, to, subject, body):
mailer.send(to=to, subject=subject, body=body)
mailer = MagicMock()
send_email(mailer, "[email protected]", "Hello", "Welcome!")
# assert mailer.send was called exactly once
# assert it was called with to="[email protected]", subject="Hello", body="Welcome!"
# print "All assertions passed" if no AssertionError
Expected Output
All assertions passedHints
Hint 1: `mock.assert_called_once_with(arg1, kwarg=val)` fails if the mock was not called exactly once with those exact arguments.
Hint 2: `mock.assert_called_with(...)` checks only the most recent call.
Hint 3: Use `mock.call_args` to inspect arguments manually if needed.
Task: Configure client.fetch with side_effect so it raises ConnectionError("Network error"). Call get_data and print the fallback message.
Solution:
from unittest.mock import MagicMock
def get_data(client):
try:
return client.fetch()
except ConnectionError as e:
print(f"Caught: {e}")
return "fallback"
client = MagicMock()
client.fetch.side_effect = ConnectionError("Network error")
result = get_data(client)
print(f"Recovered with {result}")
from unittest.mock import MagicMock
def get_data(client):
try:
return client.fetch()
except ConnectionError as e:
print(f"Caught: {e}")
return "fallback"
client = MagicMock()
# configure client.fetch to raise ConnectionError("Network error")
result = get_data(client)
print(f"Recovered with {result}")
Expected Output
Caught: Network error\nRecovered with fallbackHints
Hint 1: Set `mock.side_effect = ExceptionClass("message")` to make the mock raise an exception when called.
Hint 2: `side_effect` takes precedence over `return_value` when both are set.
Hint 3: You can also set `side_effect` to a list — each call pops the next item (exception or value).
Medium
Task: Use patch as a context manager to intercept datetime.datetime.now and return a fake time object with hour=9. Assert the greeting says "morning".
Solution:
from unittest.mock import patch, MagicMock
import datetime
def make_greeting(name):
hour = datetime.datetime.now().hour
if hour < 12:
period = "morning"
elif hour < 18:
period = "afternoon"
else:
period = "evening"
return f"Good {period}, {name}!"
fake_now = MagicMock()
fake_now.hour = 9
with patch("datetime.datetime") as mock_dt:
mock_dt.now.return_value = fake_now
greeting = make_greeting("Alice")
assert greeting == "Good morning, Alice!"
print(f"Greeting: {greeting}")
print("Timestamp verified")
from unittest.mock import patch
import datetime
def make_greeting(name):
hour = datetime.datetime.now().hour
if hour < 12:
period = "morning"
elif hour < 18:
period = "afternoon"
else:
period = "evening"
return f"Good {period}, {name}!"
# Use patch as a context manager to fix datetime.datetime.now()
# to return a time where hour == 9 (morning)
# assert make_greeting("Alice") == "Good morning, Alice!"
# print the greeting and "Timestamp verified"
Expected Output
Greeting: Good morning, Alice!\nTimestamp verifiedHints
Hint 1: Use `with patch("module.name") as mock_obj:` to patch within a block.
Hint 2: The patch is undone automatically when the `with` block exits.
Hint 3: To mock `datetime.now()`, patch `datetime.datetime` and set `return_value` on the mocked class.
Task: Set client.get.side_effect to a list where the first two items are IOError() instances and the third is the string "data". Then call fetch_with_retry.
Solution:
from unittest.mock import MagicMock
def fetch_with_retry(client, retries=3):
for attempt in range(1, retries + 1):
try:
return client.get()
except IOError:
print(f"Attempt {attempt} failed")
return None
client = MagicMock()
client.get.side_effect = [IOError(), IOError(), "data"]
result = fetch_with_retry(client)
print(f"Success: {result}")
print(f"Total attempts: {client.get.call_count}")
from unittest.mock import MagicMock
def fetch_with_retry(client, retries=3):
for attempt in range(1, retries + 1):
try:
return client.get()
except IOError:
print(f"Attempt {attempt} failed")
return None
client = MagicMock()
# first two calls raise IOError, third returns "data"
result = fetch_with_retry(client)
print(f"Success: {result}")
print(f"Total attempts: {client.get.call_count}")
Expected Output
Attempt 1 failed\nAttempt 2 failed\nSuccess: data\nTotal attempts: 3Hints
Hint 1: Setting `side_effect` to a list makes the mock return (or raise) each item in order across successive calls.
Hint 2: Put exception instances in the list for calls that should raise; put plain values for calls that should return.
Hint 3: This is perfect for testing retry logic without sleeping.
Task: Use create_autospec(Circle) to create a specced mock. Configure area to return 78.54. Attempt to call a method that doesn't exist on Circle and catch the resulting AttributeError.
Solution:
from unittest.mock import create_autospec
import math
class Circle:
def __init__(self, radius):
self.radius = radius
def area(self):
return math.pi * self.radius ** 2
def perimeter(self):
return 2 * math.pi * self.radius
mock_circle = create_autospec(Circle)
mock_circle.area.return_value = 78.54
print(f"Mocked area: {mock_circle.area()}")
spec_works = False
try:
mock_circle.nonexistent()
except AttributeError:
spec_works = True
print(f"Spec prevents wrong calls: {spec_works}")
from unittest.mock import create_autospec
import math
class Circle:
def __init__(self, radius):
self.radius = radius
def area(self):
return math.pi * self.radius ** 2
def perimeter(self):
return 2 * math.pi * self.radius
mock_circle = create_autospec(Circle)
# configure mock_circle.area() to return 78.54
# call mock_circle.area() and print the result
# try calling mock_circle.nonexistent() and catch AttributeError
# print whether spec prevented the wrong call
Expected Output
Mocked area: 78.54\nSpec prevents wrong calls: TrueHints
Hint 1: `create_autospec(SomeClass)` creates a mock that mirrors the real class interface — calling non-existent methods raises `AttributeError`.
Hint 2: This catches refactoring mistakes: if the real method is renamed, tests using `spec` break immediately.
Hint 3: Use `spec=True` or `spec=SomeClass` inside `MagicMock()` for the same effect.
Task: Use patch.object on service to replace real_fetch with a mock that returns {"user": "Bob"}. Call service.get("bob") and print the result and call count.
Solution:
from unittest.mock import patch
class DataService:
def real_fetch(self, key):
raise NotImplementedError("No DB in tests")
def get(self, key):
return self.real_fetch(key)
service = DataService()
with patch.object(service, "real_fetch", return_value={"user": "Bob"}) as mock_fetch:
result = service.get("bob")
print(f"Cache hit: {result}")
print(f"real_fetch call count: {mock_fetch.call_count}")
from unittest.mock import patch
class DataService:
def real_fetch(self, key):
# imagine this hits a real database
raise NotImplementedError("No DB in tests")
def get(self, key):
return self.real_fetch(key)
service = DataService()
# patch service.real_fetch to return {"user": "Bob"} for key "bob"
# call service.get("bob")
# print "Cache hit: <result>"
# print how many times real_fetch was called on the mock
Expected Output
Cache hit: {'user': 'Bob'}\nreal_fetch call count: 0Hints
Hint 1: `patch.object(instance_or_class, "method_name")` patches just that one method.
Hint 2: The original object remains real — only the specified method is replaced.
Hint 3: Use this when you want to test one method while keeping the rest of the class real.
Hard
Task: Patch builtins.open so it behaves as a context manager returning a mock file object whose .read() returns "hello world". Call read_file and verify the result.
Solution:
from unittest.mock import patch, MagicMock
def read_file(path):
with open(path, 'r') as f:
return f.read()
mock_file = MagicMock()
mock_file.read.return_value = "hello world"
mock_open = MagicMock()
mock_open.__enter__ = MagicMock(return_value=mock_file)
mock_open.__exit__ = MagicMock(return_value=False)
with patch("builtins.open", return_value=mock_open):
content = read_file("fake.txt")
assert content == "hello world"
print(f"File content: {content}")
print("Context manager entered and exited cleanly")
from unittest.mock import patch, MagicMock
def read_file(path):
with open(path, 'r') as f:
return f.read()
# patch builtins.open so no real file is needed
# configure it so f.read() returns "hello world"
# call read_file("fake.txt") and assert the result
# print "File content: <result>"
# print "Context manager entered and exited cleanly"
Expected Output
File content: hello world\nContext manager entered and exited cleanlyHints
Hint 1: To mock a context manager, set `mock.__enter__.return_value` to what the `as` variable should be, and `mock.__exit__.return_value = False`.
Hint 2: `MagicMock` auto-defines `__enter__` and `__exit__`, so you only need to configure their return values.
Hint 3: Patch `builtins.open` to intercept any file open call in your code.
Task: After calling batch_save, inspect storage.save.call_args_list. Print each call's args and compare the full list against the expected [call(...), call(...), call(...)] pattern.
Solution:
from unittest.mock import MagicMock, call
def batch_save(storage, files):
for name, compress in files:
storage.save(name, compress=compress)
storage = MagicMock()
files = [
("report_2023.pdf", True),
("report_2024.pdf", True),
("summary.txt", False),
]
batch_save(storage, files)
print(f"Called {storage.save.call_count} times")
for i, c in enumerate(storage.save.call_args_list, 1):
args, kwargs = c
print(f"Call {i}: save({args[0]}, compress={kwargs['compress']})")
expected = [
call("report_2023.pdf", compress=True),
call("report_2024.pdf", compress=True),
call("summary.txt", compress=False),
]
assert storage.save.call_args_list == expected
print("All call args verified")
from unittest.mock import MagicMock, call
def batch_save(storage, files):
for name, compress in files:
storage.save(name, compress=compress)
storage = MagicMock()
files = [
("report_2023.pdf", True),
("report_2024.pdf", True),
("summary.txt", False),
]
batch_save(storage, files)
# print how many times save was called
# print each call's arguments
# verify using call_args_list that all three calls match exactly
Expected Output
Called 3 times\nCall 1: save(report_2023.pdf, compress=True)\nCall 2: save(report_2024.pdf, compress=True)\nCall 3: save(summary.txt, compress=False)\nAll call args verifiedHints
Hint 1: `mock.call_args_list` is a list of `call` objects — one per invocation.
Hint 2: Use `from unittest.mock import call` to build expected call objects for comparison.
Hint 3: `call_args_list[i]` gives you the args and kwargs of the i-th call.
Task: Create a spy wrapping factorial. Call it twice directly, verify it returns real values, and inspect call_count. Explain in a comment why recursion inside factorial doesn't inflate the count.
Why it matters: The spy pattern is essential for integration tests where you need real behaviour but also want to assert that a collaborating function was called correctly.
Solution:
from unittest.mock import MagicMock
def factorial(n):
if n == 0:
return 1
return n * factorial(n - 1)
# wraps= makes spy_factorial call-through to the real factorial
spy = MagicMock(wraps=factorial)
result1 = spy(5) # real factorial(5) = 120
result2 = spy(4) # real factorial(4) = 24
result3 = spy(3) # real factorial(3) = 6
result4 = spy(1) # real factorial(1) = 1
print(f"Real result: {result1}")
# The spy only intercepts top-level calls — recursion inside factorial()
# calls the real function directly, not through the spy.
print(f"spy called {spy.call_count} times")
assert result1 == 120
assert result2 == 24
print("All calls passed through to real function")
from unittest.mock import MagicMock
import math
def factorial(n):
if n == 0:
return 1
return n * factorial(n - 1)
# create a spy that wraps factorial
spy_factorial = MagicMock(wraps=factorial)
# call spy_factorial(5) — should return real result 120
result = None # replace with actual call
# print the result
# print how many times spy_factorial was called directly (just 1 — wraps doesn't intercept recursion)
# call spy_factorial(3) so total direct calls == 2
# verify results are correct real outputs
Expected Output
Real result: 120\nspy called 4 times\nAll calls passed through to real functionHints
Hint 1: Pass `wraps=real_function` to `MagicMock` to create a spy — it calls the real function AND records call metadata.
Hint 2: This is the spy pattern: you observe without replacing behaviour.
Hint 3: Useful when you want to assert a function was called with specific args but still need the real output.
