Skip to main content
๐Ÿ›ก๏ธ Verified Technical Content: Written by Serhii Hrekov. | Last reviewed & updated in Git: August 14, 2026

Python Mocking Guide: External Dependencies, Test Doubles, and Pytest Best Practices

ยท 9 min read
Serhii Hrekov
Senior Software Engineer & System Architect specializing in Python, Web Systems, Cloud Infrastructure & Automation

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 TypePrimary MechanismVerification StyleTypical Use Case
StubReturns hardcoded canned responses.State verification (assert on function output).Providing static user config or auth tokens.
FakeWorking in-memory lightweight implementation.State verification (inspect fake repository state).In-memory SQLite or dict-backed data store.
SpyWraps real object; records invocations without modifying logic.Behavior verification (assert invocation count/arguments).Auditing metric collectors or telemetry handlers.
MockConfigured 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โ€‹