Mocking & Test Doubles
📖 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 callMagicMock()— 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 objectside_effect— configure dynamic return values, raise exceptions, or call functionsspec/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
1# ============================================================2# Mocking fundamentals with unittest.mock3# ============================================================4from unittest.mock import (5 Mock,6 MagicMock,7 patch,8 call,9 PropertyMock,10 AsyncMock,11)12import pytest13from datetime import datetime14from decimal import Decimal151617# --- Production code to test ---18class PaymentGateway:19 """Third-party payment service wrapper."""2021 def charge(self, amount, currency, token):22 # In production, this calls Stripe/PayPal API23 raise NotImplementedError("Must connect to payment provider")2425 def refund(self, transaction_id, amount=None):26 raise NotImplementedError("Must connect to payment provider")272829class OrderService:30 """Business logic that depends on PaymentGateway."""3132 def __init__(self, gateway: PaymentGateway, notifier=None):33 self.gateway = gateway34 self.notifier = notifier3536 def process_payment(self, order_id, amount, token):37 if amount <= 0:38 raise ValueError("Amount must be positive")3940 result = self.gateway.charge(41 amount=amount, currency="USD", token=token42 )4344 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"]}5051 raise RuntimeError(f"Payment failed: {result.get('error', 'Unknown')}")5253 def process_refund(self, transaction_id, amount=None):54 return self.gateway.refund(transaction_id, amount=amount)555657# ============================================================58# 1. Basic Mock usage59# ============================================================60def test_mock_gateway_charge():61 """Replace the real gateway with a Mock."""62 mock_gateway = Mock(spec=PaymentGateway)6364 # Configure the mock's return value65 mock_gateway.charge.return_value = {66 "status": "success",67 "txn_id": "txn_abc123",68 }6970 service = OrderService(gateway=mock_gateway)71 result = service.process_payment("order-1", 99.99, "tok_visa")7273 # Verify the result74 assert result["order_id"] == "order-1"75 assert result["transaction_id"] == "txn_abc123"7677 # Verify the mock was called correctly78 mock_gateway.charge.assert_called_once_with(79 amount=99.99, currency="USD", token="tok_visa"80 )818283# ============================================================84# 2. MagicMock — mock with magic method support85# ============================================================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 = 29495 # Can iterate and check length96 results = list(mock_db_results)97 assert len(mock_db_results) == 298 assert results[0]["name"] == "Alice"99100101# ============================================================102# 3. side_effect — dynamic behavior103# ============================================================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")108109 service = OrderService(gateway=mock_gateway)110 with pytest.raises(ConnectionError, match="Network timeout"):111 service.process_payment("order-1", 50.00, "tok_visa")112113114def 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 call119 ConnectionError("Server down"), # second call120 {"status": "success", "txn_id": "txn_003"}, # third call121 ]122123 service = OrderService(gateway=mock_gateway)124125 # First call succeeds126 result = service.process_payment("o1", 10, "tok1")127 assert result["transaction_id"] == "txn_001"128129 # Second call raises130 with pytest.raises(ConnectionError):131 service.process_payment("o2", 20, "tok2")132133 # Third call succeeds again134 result = service.process_payment("o3", 30, "tok3")135 assert result["transaction_id"] == "txn_003"136137138def test_side_effect_function():139 """side_effect with callable for dynamic logic."""140 mock_gateway = Mock(spec=PaymentGateway)141142 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:]}"}146147 mock_gateway.charge.side_effect = dynamic_charge148149 service = OrderService(gateway=mock_gateway)150 result = service.process_payment("o1", 500, "tok_visa")151 assert result["transaction_id"] == "txn_visa"152153 with pytest.raises(RuntimeError, match="Amount exceeds limit"):154 service.process_payment("o2", 15_000, "tok_visa")155156157# ============================================================158# 4. patch() — replace objects at their lookup location159# ============================================================160# Suppose order_service.py imports: from datetime import datetime161162# WRONG: @patch("datetime.datetime") <-- patches where defined163# RIGHT: @patch("__main__.datetime") <-- patches where looked up164165@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)170171 assert datetime.now().year == 2025172 assert datetime.now().month == 1173174175def 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()186187188# ============================================================189# 5. spec and autospec — type-safe mocking190# ============================================================191def test_spec_catches_typos():192 """spec= constrains mock to the real object's interface."""193 mock_gateway = Mock(spec=PaymentGateway)194195 # This works — 'charge' exists on PaymentGateway196 mock_gateway.charge.return_value = {"status": "success", "txn_id": "x"}197198 # This raises AttributeError — 'chrage' is a typo!199 with pytest.raises(AttributeError):200 mock_gateway.chrage(100, "USD", "tok")201202203# ============================================================204# 6. Verifying call patterns205# ============================================================206def test_call_verification():207 """Rich assertion API for verifying mock interactions."""208 mock_notifier = Mock()209210 mock_gateway = Mock(spec=PaymentGateway)211 mock_gateway.charge.return_value = {212 "status": "success",213 "txn_id": "txn_verify",214 }215216 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")219220 # Verify call count221 assert mock_gateway.charge.call_count == 2222223 # Verify specific calls224 mock_gateway.charge.assert_any_call(225 amount=50, currency="USD", token="tok_1"226 )227228 # Verify call order229 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)234235 # Verify notifier was called twice236 assert mock_notifier.send.call_count == 2237238239# ============================================================240# 7. PropertyMock — mock properties241# ============================================================242def test_property_mock():243 """Mock a property on a class."""244 with patch.object(245 OrderService, "gateway", new_callable=PropertyMock246 ) as mock_prop:247 mock_prop.return_value = "mock_gateway_value"248 service = OrderService.__new__(OrderService)249 assert service.gateway == "mock_gateway_value"250251252# ============================================================253# Run: pytest test_mocking.py -v254# ============================================================
🏋️ Practice Exercise
Exercises:
Create a
WeatherServicethat calls an external API. Write tests usingMockandpatchto simulate successful responses, network errors, malformed JSON, and timeout scenarios without making real HTTP calls.Test a function that reads a CSV file and returns summary statistics. Mock
builtins.openwithmock_opento provide fake CSV content. Verify the function handles empty files and malformed rows gracefully.Use
side_effectwith 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.Refactor a test suite that uses
Mock()withoutspecto useMock(spec=RealClass). Identify at least two bugs thatspeccatches (wrong method names, wrong argument counts) that plainMocksilently ignores.Write tests for an
EmailServicethat sends emails via SMTP. Mock the SMTP client to verify: correct recipients, subject line, body content, and thatquit()is always called (even when sending fails). Usepatchas both a decorator and context manager.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.pydoesfrom requests import get, you must patchservice.get, notrequests.get. The import binds the name in the importing module's namespace.Not using
spec=orautospec=Trueon mocks. Without spec, mocks silently accept any attribute or method call, meaning typos likemock.clse()instead ofmock.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_valuewhenside_effectis needed.return_valuegives the same result every call. Useside_effectfor sequences, exceptions, or dynamic logic based on arguments.
💼 Interview Questions
🎤 Mock Interview
Practice a live interview for Mocking & Test Doubles