Skip to main content

Python Mocking and Test Doubles Practice Problems & Exercises

Practice: Mocking and Test Doubles

11 problems4 Easy4 Medium3 Hard50–65 min
← Back to lesson

Easy

#1MagicMock Basics — return_valueEasy
mockMagicMockreturn_value

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\n1
Hints

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.


#2patch as a DecoratorEasy
mockpatchdecoratorrequests

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: ok
Hints

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.


#3assert_called_with — Verifying Call ArgumentsEasy
mockassert_called_withcall-verification

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()
send_email(mailer, "[email protected]", "Hello", "Welcome!")

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 passed
Hints

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.


#4side_effect — Raising Exceptions from MocksEasy
mockside_effectexceptions

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 fallback
Hints

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

#5patch as a Context ManagerMedium
mockpatchcontext-managerdatetime

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 verified
Hints

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.


#6side_effect as a List — Sequential Return ValuesMedium
mockside_effectlistsequential

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: 3
Hints

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.


#7Patching an Object's Method with specMedium
mockspeccreate_autospecsafety

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: True
Hints

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.


#8patch.object — Mocking a Single Method on a Real ObjectMedium
mockpatch.objectmethod-patching

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: 0
Hints

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

#9Mock a Context ManagerHard
mockcontext-manager__enter____exit__MagicMock

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 cleanly
Hints

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.


#10call_args_list — Verifying Multiple CallsHard
mockcall_args_listcallmultiple-calls

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 verified
Hints

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.


#11Spy Pattern — Wrapping a Real FunctionHard
mockwrapsspycall-through

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 function
Hints

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.

© 2026 EngineersOfAI. All rights reserved.