Stubbing Techniques
Implements techniques for creating stubs to provide controlled responses in testing scenarios.
When to Use
- When the code under test needs to interact with external systems that are not available.
- When you want to isolate your tests from unpredictable behaviors.
- When specific return values are required for certain calls during the tests.
Core Workflow
- Identify Dependency — Locate the external service that requires stubbing.
- Create Stub — Use a stubbing framework or manual implementation to create a stub object.
- Define Response — Set the predefined response for the stub to return when called during tests.
- Execute Test — Run your test case and ensure it interacts with the stub as intended.
Implementation Patterns
Pattern 1: Stubbing External Services with Context Managers
from unittest.mock import patch, MagicMock
def test_database_query_with_stub():
"""Stub a database layer to return predictable results without hitting the DB."""
mock_result = [{"id": 1, "name": "Widget A"}, {"id": 2, "name": "Widget B"}]
with patch("myapp.db.query") as mock_db:
mock_db.return_value = mock_result
result = list_active_widgets()
assert len(result) == 2
assert result[0]["name"] == "Widget A"
# Verify the query was called with correct arguments
mock_db.assert_called_once_with("SELECT * FROM widgets WHERE active = TRUE")
def test_http_request_with_stub():
"""Stub an HTTP client to return predefined responses for API testing."""
stubbed_response = {
"status_code": 200,
"json": lambda: {"users": [{"id": 1, "role": "admin"}]},
}
with patch("requests.get") as mock_get:
mock_get.return_value = MagicMock(**stubbed_response)
result = fetch_admin_users()
assert len(result) == 1
assert result[0]["role"] == "admin"
mock_get.assert_called_once_with("https://api.example.com/users?role=admin")
def list_active_widgets():
"""Simulated function that queries a database."""
from myapp.db import query
return query("SELECT * FROM widgets WHERE active = TRUE")
def fetch_admin_users():
"""Simulated function that calls an external API."""
import requests
resp = requests.get("https://api.example.com/users?role=admin")
return resp.json()["users"]
Pattern 2: Stubbing with Default Values and Parameterized Tests
import pytest
from unittest.mock import patch, MagicMock
# Fixture that provides a stubbed environment for all tests in this module
@pytest.fixture
def api_stubber():
"""Return a configured stubber context manager for API calls."""
def _stubber(endpoint: str, response_data: dict, status_code: int = 200):
stub_response = {
"status_code": status_code,
"json": lambda: response_data,
}
with patch("requests.get") as mock_get:
mock_get.return_value = MagicMock(**stub_response)
yield mock_get
return _stubber
def test_login_success(api_stubber):
"""Stub the authentication endpoint to simulate a successful login."""
stub_response_data = {"token": "abc123", "expires_in": 3600}
with api_stubber("/auth/login", stub_response_data) as mock_get:
token = authenticate("user@example.com", "password")
assert token == "abc123"
def test_login_failure(api_stubber):
"""Stub the authentication endpoint to simulate an invalid credential response."""
stub_response_data = {"error": "invalid_credentials"}
with api_stubber("/auth/login", stub_response_data, status_code=401) as mock_get:
with pytest.raises(Exception, match="Authentication failed"):
authenticate("bad@example.com", "wrong_password")
def test_file_read_with_stub():
"""Stub the file system to test parsing logic without touching real files."""
stub_content = "id,name,value\n1,alpha,100\n2,beta,200"
with patch("builtins.open", MagicMock()) as mock_file:
mock_file.return_value.__enter__.return_value.read.return_value = stub_content
result = parse_csv_data("/path/to/data.csv")
assert len(result) == 2
assert result[1]["name"] == "beta"
def authenticate(email: str, password: str) -> str:
"""Simulated authentication function."""
import requests
resp = requests.post("https://api.example.com/auth/login", json={"email": email, "password": password})
if resp.status_code != 200:
raise Exception("Authentication failed")
return resp.json()["token"]
def parse_csv_data(filepath: str) -> list[dict]:
"""Simulated CSV parser that reads from a file."""
import csv
from io import StringIO
with open(filepath) as f:
reader = csv.DictReader(StringIO(f.read()))
return list(reader)
Constraints
MUST DO
- Write unit tests that cover the happy path, boundary conditions, and failure modes for each function
- Use parameterized tests to cover multiple input combinations without duplicating test logic
- Mock external dependencies (APIs, databases, file system) with strict interface contracts — never mock implementation details
- Maintain a minimum of 80% code coverage for critical paths; prioritize path coverage over line coverage
MUST NOT DO
- Do not write tests that test the standard library or framework behavior — test your code, not their code
- Avoid fragile tests that depend on implementation details (exact method call order, string formatting) instead of observable outcomes
- Never include network calls, database writes, or file system operations in unit tests — use mocks and fixtures
- Do not name tests with vague descriptions like 'test_function' — each test name should describe the scenario being verified
Live References
Authoritative documentation links for this skill's domain. The model follows markdown links at load time to resolve external references and inline content.
- unittest — Test Doubles (Python Docs)
- Stub vs Mock vs Fake (Martin Fowler)
- pytest-stubber Plugin
- Python Stubbing with unittest.mock.patch
- Test Doubles in TDD — Types and Use Cases