Mocking & Test Doubles

0/4 in this phase0/54 across the roadmap

📖 Concept

Mocking is the practice of replacing real dependencies with controlled substitutes during testing. Python's unittest.mock module (part of the standard library since Python 3.3) provides powerful tools for creating test doubles — objects that stand in for real components like databases, APIs, file systems, and third-party services.

Types of test doubles:

Double Purpose Example
Mock Records calls, returns configurable values API client substitute
Stub Returns predetermined data Hardcoded DB query result
Spy Wraps real object, records interactions Logging call verification
Fake Simplified working implementation In-memory database

Core unittest.mock tools:

  • Mock() — general-purpose mock object that accepts any attribute access or method call
  • MagicMock() — Mock with default implementations of magic methods (__len__, __iter__, etc.)
  • patch() — temporarily replaces an object at its lookup location (critical concept)
  • patch.object() — patches a specific attribute on an object
  • side_effect — configure dynamic return values, raise exceptions, or call functions
  • spec / autospec — constrain the mock to the interface of the real object, catching typos and incorrect call signatures

The golden rule of patching: Always patch where the object is looked up, not where it is defined. If module_a imports requests.get and you want to mock it in tests for module_a, you patch module_a.requests.get, NOT requests.get.

When to mock:

  • External services (HTTP APIs, databases, message queues)
  • Time-dependent behavior (datetime.now(), time.sleep())
  • Filesystem operations (reading/writing files)
  • Non-deterministic outputs (random numbers, UUIDs)

When NOT to mock:

  • Simple data transformations (pure functions)
  • The system under test itself
  • So extensively that tests pass with broken production code (over-mocking)

💻 Code Example

codeTap to expand ⛶
1# ============================================================
2# Mocking fundamentals with unittest.mock
3# ============================================================
4from unittest.mock import (
5 Mock,
6 MagicMock,
7 patch,
8 call,
9 PropertyMock,
10 AsyncMock,
11)
12import pytest
13from datetime import datetime
14from decimal import Decimal
15
16
17# --- Production code to test ---
18class PaymentGateway:
19 """Third-party payment service wrapper."""
20
21 def charge(self, amount, currency, token):
22 # In production, this calls Stripe/PayPal API
23 raise NotImplementedError("Must connect to payment provider")
24
25 def refund(self, transaction_id, amount=None):
26 raise NotImplementedError("Must connect to payment provider")
27
28
29class OrderService:
30 """Business logic that depends on PaymentGateway."""
31
32 def __init__(self, gateway: PaymentGateway, notifier=None):
33 self.gateway = gateway
34 self.notifier = notifier
35
36 def process_payment(self, order_id, amount, token):
37 if amount <= 0:
38 raise ValueError("Amount must be positive")
39
40 result = self.gateway.charge(
41 amount=amount, currency="USD", token=token
42 )
43
44 if result["status"] == "success":
45 if self.notifier:
46 self.notifier.send(
47 f"Payment of ${amount} for order {order_id} succeeded"
48 )
49 return {"order_id": order_id, "transaction_id": result["txn_id"]}
50
51 raise RuntimeError(f"Payment failed: {result.get('error', 'Unknown')}")
52
53 def process_refund(self, transaction_id, amount=None):
54 return self.gateway.refund(transaction_id, amount=amount)
55
56
57# ============================================================
58# 1. Basic Mock usage
59# ============================================================
60def test_mock_gateway_charge():
61 """Replace the real gateway with a Mock."""
62 mock_gateway = Mock(spec=PaymentGateway)
63
64 # Configure the mock's return value
65 mock_gateway.charge.return_value = {
66 "status": "success",
67 "txn_id": "txn_abc123",
68 }
69
70 service = OrderService(gateway=mock_gateway)
71 result = service.process_payment("order-1", 99.99, "tok_visa")
72
73 # Verify the result
74 assert result["order_id"] == "order-1"
75 assert result["transaction_id"] == "txn_abc123"
76
77 # Verify the mock was called correctly
78 mock_gateway.charge.assert_called_once_with(
79 amount=99.99, currency="USD", token="tok_visa"
80 )
81
82
83# ============================================================
84# 2. MagicMock — mock with magic method support
85# ============================================================
86def test_magic_mock_iteration():
87 """MagicMock supports __iter__, __len__, __getitem__, etc."""
88 mock_db_results = MagicMock()
89 mock_db_results.__iter__.return_value = iter([
90 {"id": 1, "name": "Alice"},
91 {"id": 2, "name": "Bob"},
92 ])
93 mock_db_results.__len__.return_value = 2
94
95 # Can iterate and check length
96 results = list(mock_db_results)
97 assert len(mock_db_results) == 2
98 assert results[0]["name"] == "Alice"
99
100
101# ============================================================
102# 3. side_effect — dynamic behavior
103# ============================================================
104def test_side_effect_exception():
105 """side_effect with exception simulates failures."""
106 mock_gateway = Mock(spec=PaymentGateway)
107 mock_gateway.charge.side_effect = ConnectionError("Network timeout")
108
109 service = OrderService(gateway=mock_gateway)
110 with pytest.raises(ConnectionError, match="Network timeout"):
111 service.process_payment("order-1", 50.00, "tok_visa")
112
113
114def test_side_effect_sequence():
115 """side_effect with list returns values in sequence."""
116 mock_gateway = Mock(spec=PaymentGateway)
117 mock_gateway.charge.side_effect = [
118 {"status": "success", "txn_id": "txn_001"}, # first call
119 ConnectionError("Server down"), # second call
120 {"status": "success", "txn_id": "txn_003"}, # third call
121 ]
122
123 service = OrderService(gateway=mock_gateway)
124
125 # First call succeeds
126 result = service.process_payment("o1", 10, "tok1")
127 assert result["transaction_id"] == "txn_001"
128
129 # Second call raises
130 with pytest.raises(ConnectionError):
131 service.process_payment("o2", 20, "tok2")
132
133 # Third call succeeds again
134 result = service.process_payment("o3", 30, "tok3")
135 assert result["transaction_id"] == "txn_003"
136
137
138def test_side_effect_function():
139 """side_effect with callable for dynamic logic."""
140 mock_gateway = Mock(spec=PaymentGateway)
141
142 def dynamic_charge(amount, currency, token):
143 if amount > 10_000:
144 return {"status": "failed", "error": "Amount exceeds limit"}
145 return {"status": "success", "txn_id": f"txn_{token[-4:]}"}
146
147 mock_gateway.charge.side_effect = dynamic_charge
148
149 service = OrderService(gateway=mock_gateway)
150 result = service.process_payment("o1", 500, "tok_visa")
151 assert result["transaction_id"] == "txn_visa"
152
153 with pytest.raises(RuntimeError, match="Amount exceeds limit"):
154 service.process_payment("o2", 15_000, "tok_visa")
155
156
157# ============================================================
158# 4. patch() — replace objects at their lookup location
159# ============================================================
160# Suppose order_service.py imports: from datetime import datetime
161
162# WRONG: @patch("datetime.datetime") <-- patches where defined
163# RIGHT: @patch("__main__.datetime") <-- patches where looked up
164
165@patch("__main__.datetime")
166def test_patch_datetime(mock_dt):
167 """Patch datetime to control 'now()'."""
168 mock_dt.now.return_value = datetime(2025, 1, 15, 10, 30, 0)
169 mock_dt.side_effect = lambda *a, **kw: datetime(*a, **kw)
170
171 assert datetime.now().year == 2025
172 assert datetime.now().month == 1
173
174
175def test_patch_as_context_manager():
176 """patch() works as a context manager too."""
177 with patch.object(PaymentGateway, "charge") as mock_charge:
178 mock_charge.return_value = {
179 "status": "success",
180 "txn_id": "txn_ctx",
181 }
182 gateway = PaymentGateway()
183 result = gateway.charge(100, "USD", "tok_test")
184 assert result["txn_id"] == "txn_ctx"
185 mock_charge.assert_called_once()
186
187
188# ============================================================
189# 5. spec and autospec — type-safe mocking
190# ============================================================
191def test_spec_catches_typos():
192 """spec= constrains mock to the real object's interface."""
193 mock_gateway = Mock(spec=PaymentGateway)
194
195 # This works — 'charge' exists on PaymentGateway
196 mock_gateway.charge.return_value = {"status": "success", "txn_id": "x"}
197
198 # This raises AttributeError'chrage' is a typo!
199 with pytest.raises(AttributeError):
200 mock_gateway.chrage(100, "USD", "tok")
201
202
203# ============================================================
204# 6. Verifying call patterns
205# ============================================================
206def test_call_verification():
207 """Rich assertion API for verifying mock interactions."""
208 mock_notifier = Mock()
209
210 mock_gateway = Mock(spec=PaymentGateway)
211 mock_gateway.charge.return_value = {
212 "status": "success",
213 "txn_id": "txn_verify",
214 }
215
216 service = OrderService(gateway=mock_gateway, notifier=mock_notifier)
217 service.process_payment("order-1", 50, "tok_1")
218 service.process_payment("order-2", 75, "tok_2")
219
220 # Verify call count
221 assert mock_gateway.charge.call_count == 2
222
223 # Verify specific calls
224 mock_gateway.charge.assert_any_call(
225 amount=50, currency="USD", token="tok_1"
226 )
227
228 # Verify call order
229 expected_calls = [
230 call(amount=50, currency="USD", token="tok_1"),
231 call(amount=75, currency="USD", token="tok_2"),
232 ]
233 mock_gateway.charge.assert_has_calls(expected_calls, any_order=False)
234
235 # Verify notifier was called twice
236 assert mock_notifier.send.call_count == 2
237
238
239# ============================================================
240# 7. PropertyMock — mock properties
241# ============================================================
242def test_property_mock():
243 """Mock a property on a class."""
244 with patch.object(
245 OrderService, "gateway", new_callable=PropertyMock
246 ) as mock_prop:
247 mock_prop.return_value = "mock_gateway_value"
248 service = OrderService.__new__(OrderService)
249 assert service.gateway == "mock_gateway_value"
250
251
252# ============================================================
253# Run: pytest test_mocking.py -v
254# ============================================================

🏋️ Practice Exercise

Exercises:

  1. Create a WeatherService that calls an external API. Write tests using Mock and patch to simulate successful responses, network errors, malformed JSON, and timeout scenarios without making real HTTP calls.

  2. Test a function that reads a CSV file and returns summary statistics. Mock builtins.open with mock_open to provide fake CSV content. Verify the function handles empty files and malformed rows gracefully.

  3. Use side_effect with a list to simulate a retry pattern: a function calls an unreliable API that fails twice then succeeds on the third attempt. Assert the function returns the successful result after exactly 3 attempts.

  4. Refactor a test suite that uses Mock() without spec to use Mock(spec=RealClass). Identify at least two bugs that spec catches (wrong method names, wrong argument counts) that plain Mock silently ignores.

  5. Write tests for an EmailService that sends emails via SMTP. Mock the SMTP client to verify: correct recipients, subject line, body content, and that quit() is always called (even when sending fails). Use patch as both a decorator and context manager.

  6. Create a test that patches datetime.now() to return a fixed time and verify time-dependent business logic (e.g., a function that returns different messages for morning, afternoon, and evening).

⚠️ Common Mistakes

  • Patching where the object is defined instead of where it is looked up. If service.py does from requests import get, you must patch service.get, not requests.get. The import binds the name in the importing module's namespace.

  • Not using spec= or autospec=True on mocks. Without spec, mocks silently accept any attribute or method call, meaning typos like mock.clse() instead of mock.close() pass without error and hide real bugs.

  • Over-mocking — replacing so many components that the test only verifies the mock wiring, not actual behavior. If your test passes even when the production code is completely broken, you are testing mocks, not code.

  • Forgetting to assert that mocks were actually called. A mock that is never called means the code path was never exercised. Always pair mocks with assert_called_once_with() or equivalent verification.

  • Using return_value when side_effect is needed. return_value gives the same result every call. Use side_effect for sequences, exceptions, or dynamic logic based on arguments.

💼 Interview Questions

🎤 Mock Interview

Practice a live interview for Mocking & Test Doubles