Python Mocking Guide: External Dependencies, Test Doubles, and Pytest Best Practices
External dependencies are a major source of pain in unit testing. Network calls, persistent databases, and file systems are inherently slow, non-deterministic, and prone to environmental failures. Mocking these boundaries isolates your unit tests and delivers sub-second test execution suites.
However, over-mocking leads to brittle tests that pass while production code breaks. This guide provides a complete manual on Python test doubles: the architectural framework of when to mock, test double taxonomy (stubs, fakes, spies, mocks), hands-on unittest.mock examples for HTTP APIs, databases, and file systems, plus Pytest mocking pitfalls to avoid.
Architectural Framework: When to Mock vs. When to Use Real Dependenciesโ
The golden rule of test isolation is: only mock what you don't own or boundaries that introduce non-determinism (external APIs, third-party services, file I/O, system clock).
+-----------------------------------+
| Is the dependency external/slow? |
+-----------------+-----------------+
|
+----------------+----------------+
| |
Yes No
| |
+--------------+--------------+ +-----------+-----------+
| Complex state / data flow? | | Is it pure logic / |
+-------+--------------+------+ | fast standard lib? |
| | +-----------+-----------+
Yes No |
| | Yes
+-----+----+ +-----+----+ |
| Use FAKE | | Use MOCK | +-----+------+
| (In-Mem) | | (Patch) | | Use REAL |
+----------+ +----------+ | Dependency |
+------------+
When to Use Mocksโ
- External HTTP APIs & Third-Party Services: Stripe, SendGrid, AWS S3, or partner webhooks.
- Side-Effect Verification: Verifying that an email notification was triggered with precise recipient parameters.
When to Use Fakesโ
- Stateful Repositories / In-Memory Databases: A dictionary-based repository simulating SQL queries without network latency.
- Complex Multi-Step State: When setting up return values across 10 chained method calls becomes brittle and unreadable.
When to Use Real Objectsโ
- Fast Standard Library Utilities:
datetime,math,urllib.parse, and string manipulation. - Domain Data Models: Pydantic models, dataclasses, and pure business logic functions.
The Taxonomy of Test Doublesโ
The term "mock" is frequently used as a generic term, but test doubles comprise four distinct roles:
| Double Type | Primary Mechanism | Verification Style | Typical Use Case |
|---|---|---|---|
| Stub | Returns hardcoded canned responses. | State verification (assert on function output). | Providing static user config or auth tokens. |
| Fake | Working in-memory lightweight implementation. | State verification (inspect fake repository state). | In-memory SQLite or dict-backed data store. |
| Spy | Wraps real object; records invocations without modifying logic. | Behavior verification (assert invocation count/arguments). | Auditing metric collectors or telemetry handlers. |
| Mock | Configured with pre-programmed expectations. | Behavior verification (assert_called_once_with). | External HTTP client or payment gateway. |
Practical Implementation: Mocking Common External Boundariesโ
1. Mocking External HTTP API Callsโ
Making real network calls during automated testing creates brittle pipelines dependent on external uptime and rate limits.
api_client.py
import requests
def get_users():
"""Fetches user list from remote API endpoint."""
try:
response = requests.get("https://api.example.com/users", timeout=5)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
print(f"API Error: {e}")
return None
test_api_client.py
import unittest
from unittest.mock import patch, MagicMock
import requests
from api_client import get_users
class TestAPIClient(unittest.TestCase):
# Patch requests.get where it is imported inside api_client
@patch("api_client.requests.get")
def test_get_users_success(self, mock_get):
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = [{"id": 1, "name": "Alice"}]
mock_response.raise_for_status.return_value = None
mock_get.return_value = mock_response
users = get_users()
mock_get.assert_called_once_with("https://api.example.com/users", timeout=5)
self.assertEqual(len(users), 1)
self.assertEqual(users[0]["name"], "Alice")
@patch("api_client.requests.get")
def test_get_users_api_error(self, mock_get):
mock_get.side_effect = requests.exceptions.HTTPError("500 Server Error")
users = get_users()
self.assertIsNone(users)
2. Mocking Database Clients and Cursorsโ
Chained database operations (connection.cursor().fetchone()) can be intercepted using chained MagicMock instances.
db_client.py
import sqlite3
class DatabaseClient:
def __init__(self, db_path: str):
self.conn = sqlite3.connect(db_path)
self.cursor = self.conn.cursor()
def get_user_by_id(self, user_id: int):
self.cursor.execute("SELECT name FROM users WHERE id=?", (user_id,))
result = self.cursor.fetchone()
return result[0] if result else None
test_db_client.py
import unittest
from unittest.mock import patch, MagicMock
from db_client import DatabaseClient
class TestDatabaseClient(unittest.TestCase):
@patch("db_client.sqlite3.connect")
def test_get_user_by_id_exists(self, mock_connect):
mock_cursor = MagicMock()
mock_connect.return_value.cursor.return_value = mock_cursor
mock_cursor.fetchone.return_value = ("Alice",)
client = DatabaseClient("test_db.sqlite")
name = client.get_user_by_id(1)
mock_cursor.execute.assert_called_once_with("SELECT name FROM users WHERE id=?", (1,))
self.assertEqual(name, "Alice")
@patch("db_client.sqlite3.connect")
def test_get_user_by_id_not_found(self, mock_connect):
mock_cursor = MagicMock()
mock_connect.return_value.cursor.return_value = mock_cursor
mock_cursor.fetchone.return_value = None
client = DatabaseClient("test_db.sqlite")
name = client.get_user_by_id(999)
self.assertIsNone(name)
3. Mocking File System Operations (mock_open)โ
The mock_open helper replaces standard Python file descriptors without creating temporary files on disk.
file_operations.py
def read_config(file_path: str) -> str:
with open(file_path, "r", encoding="utf-8") as f:
return f.read()
def write_log(message: str, file_path: str = "app.log"):
with open(file_path, "a", encoding="utf-8") as f:
f.write(message + "\n")
test_file_operations.py
import unittest
from unittest.mock import patch, mock_open
from file_operations import read_config, write_log
class TestFileOperations(unittest.TestCase):
@patch("builtins.open", new_callable=mock_open, read_data="app_env: production")
def test_read_config(self, mock_file):
config = read_config("config.yml")
self.assertEqual(config, "app_env: production")
mock_file.assert_called_once_with("config.yml", "r", encoding="utf-8")
@patch("builtins.open", new_callable=mock_open)
def test_write_log(self, mock_file):
write_log("User registered")
mock_file.assert_called_once_with("app.log", "a", encoding="utf-8")
mock_file.return_value.write.assert_called_once_with("User registered\n")
Pytest Mocking Pitfalls and Best Practicesโ
1. Where to Patch (Patch Where It's Imported)โ
Always patch the target object in the namespace where it is imported and used, not where it is originally defined.
# BAD: Patching definition module has no effect if app.services imported the symbol directly
mocker.patch("os.path.exists", return_value=True)
# GOOD: Patching consumer namespace
mocker.patch("app.services.exists", return_value=True)
2. Prefer Pytest mocker Fixture Over Manual Context Managersโ
Using the pytest-mock plugin's mocker fixture guarantees automatic unpatching and cleanup after every test, preventing leaked mocks across test suites:
def test_payment_processing(mocker):
mock_charge = mocker.patch("app.billing.stripe_client.charge", return_value={"status": "succeeded"})
result = process_order(order_id=42, amount=100)
assert result.is_paid is True
mock_charge.assert_called_once_with(amount=100)
Sources & Technical Referencesโ
- [1] Martin Fowler: Mocks Aren't Stubs - Foundations of test double taxonomy.
- [2] Python Documentation: unittest.mock - mock object library - Standard library mock documentation.
- [3] Python Documentation: Where to patch - Official guide on target path resolution.
- [4] Real Python: The Python Mocking Library - Comprehensive guide to mocks, patches, and magic methods.
- [5] Test-Driven Development with Python: Mocking Guidelines - Strategic testing boundaries.
- [6] Pytest Documentation: pytest-mock Plugin Guide - Fixture-based isolation and assertions.
- [7] Video Tutorial: Intro to Python Mocks - Visual walkthrough of HTTP mocking.
