19.7. Mocking
Table of Contents
Why Mocking Matters
When you test backend code, your functions often talk to things outside your codebase:
- Databases
- HTTP APIs
- Message queues
- File systems
- Email providers
- Time, random number generators, environment variables
Those “external” things are:
- Slow
- Hard to control (network errors, changing data)
- Sometimes expensive (paid APIs)
- Not available in CI or local environments
Mocking lets you replace those real dependencies with fake objects that:
- Behave in a predictable way
- Are fully under your control
- Can record how they were used
So you can:
- Test logic without hitting real services
- Simulate errors easily
- Run fast and reliable tests
What Is a Mock?
A mock is a stand‑in object that you plug in instead of the real thing.
You define what the mock should:
- Return when called
- Raise as exceptions
- Record: how many times it was called, with which arguments
In Python, the standard tool is unittest.mock, especially Mock and patch. With pytest you can also use pytest-mock, which wraps unittest.mock in a convenient fixture.
Example conceptually:
def send_welcome_email(email_client, user_email):
email_client.send(
to=user_email,
subject="Welcome",
body="Hello!"
)
return TrueFor a test you do not want to send real emails. Instead:
from unittest.mock import Mock
def test_send_welcome_email():
fake_email_client = Mock()
result = send_welcome_email(fake_email_client, "user@example.com")
assert result is True
fake_email_client.send.assert_called_once_with(
to="user@example.com",
subject="Welcome",
body="Hello!"
)The mock captures the call, and the test asserts the behavior.
Types of Test Doubles
“Mock” is often used loosely, but there are several related concepts, sometimes called test doubles.
| Term | What it does | Typical use |
|---|---|---|
| Dummy | Passed but never used | Just to satisfy a function signature |
| Stub | Returns fixed data, no assertions | Making code think it got a response |
| Spy | Records how it was used | Verifying calls and arguments |
| Mock | Can stub behavior and verify calls | Common general-purpose fake |
| Fake | Simple working implementation, not production‑ready | In‑memory DB, simple in‑memory cache, etc. |
In practice, libraries like unittest.mock.Mock can be used as stub, spy, or mock.
When to Use Mocking (and When Not To)
Use mocking when:
- The real dependency is slow or flaky
- The behavior is hard to reproduce (timeouts, API outages)
- You need to test logic that depends on how a dependency is called
- You are writing unit tests that should not cross process boundaries
Avoid or limit mocking when:
- You are writing integration tests that must test the “real thing”
- You start mocking so much that your test knows too much about implementation details
- You could simply inject a small fake implementation instead
Rule: Mock external boundaries (network, database, file system), not your own business logic. If you mock too much internal code, your tests will be fragile and break on simple refactors.
Basic Mocking with `unittest.mock.Mock`
A Mock object:
- Accepts any attribute or method access
- Records how it was used
- Lets you configure return values and side effects
from unittest.mock import Mock
# Create a mock
db = Mock()
# Configure return value for method
db.get_user_by_id.return_value = {"id": 1, "email": "test@example.com"}
# Use it
user = db.get_user_by_id(1)
assert user["email"] == "test@example.com"
# Verify interaction
db.get_user_by_id.assert_called_once_with(1)You can also configure side effects:
from unittest.mock import Mock
api = Mock()
# Raise an exception when called
api.fetch_data.side_effect = TimeoutError("Request timed out")
def test_api_timeout():
try:
api.fetch_data()
except TimeoutError:
passUseful attributes and methods:
| Feature | Example | Purpose |
|---|---|---|
| Set return value | mock.method.return_value = 42 | Simple return |
| Set side effect (exception) | mock.method.side_effect = ValueError() | Simulate errors |
| Record call count | mock.method.call_count | How many times called |
| Check exact call | mock.method.assert_called_once_with(...) | Verify interaction |
| Get all calls | mock.method.call_args_list | Detailed inspection |
Patching with `unittest.mock.patch`
Sometimes you need to replace where the dependency is used, not where it is defined.
Imagine a module:
# user_service.py
from external_email import EmailClient
def register_user(email):
client = EmailClient()
client.send(to=email, subject="Welcome", body="Hello!")
In the test, creating a real EmailClient is not desired. You patch it in the module that uses it:
# test_user_service.py
from unittest.mock import patch
from user_service import register_user
@patch("user_service.EmailClient")
def test_register_user_sends_email(mock_email_client_cls):
mock_instance = mock_email_client_cls.return_value
register_user("user@example.com")
mock_instance.send.assert_called_once_with(
to="user@example.com",
subject="Welcome",
body="Hello!"
)Key points:
- The patch target string is
"user_service.EmailClient"
Not"external_email.EmailClient" mock_email_client_clsis the mock of the class, its.return_valueis the instance used inside the function.
Using `patch` as a Context Manager
from unittest.mock import patch
def test_time_dependent_code():
with patch("my_module.time.time", return_value=1234567890):
from my_module import get_timestamp_string
ts = get_timestamp_string()
assert ts == "1234567890"You can also use:
with patch("my_module.time.time") as mock_time:
mock_time.return_value = 1234567890Mocking in `pytest` with `mocker`
If you use pytest, the pytest-mock plugin gives a mocker fixture that is convenient.
Example:
# app/email_utils.py
import smtplib
def send_email(to, subject, body):
with smtplib.SMTP("smtp.example.com") as client:
message = f"Subject: {subject}\n\n{body}"
client.sendmail("noreply@example.com", [to], message)Test:
# test_email_utils.py
def test_send_email(mocker):
mock_smtp_cls = mocker.patch("app.email_utils.smtplib.SMTP")
mock_smtp = mock_smtp_cls.return_value.__enter__.return_value
from app.email_utils import send_email
send_email("user@example.com", "Hi", "Hello")
mock_smtp.sendmail.assert_called_once()
mocker.patch is basically unittest.mock.patch, but integrated with fixtures.
Mocking HTTP Calls
Backend services often call external APIs. You do not want real network calls in unit tests.
Example function:
# weather_client.py
import requests
def get_temperature(city):
resp = requests.get(
"https://api.weather.example.com/v1/weather",
params={"city": city},
timeout=5,
)
resp.raise_for_status()
data = resp.json()
return data["temperature"]
Test using patch:
from unittest.mock import Mock, patch
from weather_client import get_temperature
@patch("weather_client.requests.get")
def test_get_temperature(mock_get):
mock_resp = Mock()
mock_resp.json.return_value = {"temperature": 21}
mock_resp.raise_for_status.return_value = None
mock_get.return_value = mock_resp
temp = get_temperature("Berlin")
assert temp == 21
mock_get.assert_called_once_with(
"https://api.weather.example.com/v1/weather",
params={"city": "Berlin"},
timeout=5,
)You are checking:
- Correct URL and parameters
- JSON parsing and return value
To simulate errors:
@patch("weather_client.requests.get")
def test_get_temperature_handles_error(mock_get):
mock_resp = Mock()
mock_resp.raise_for_status.side_effect = Exception("Error")
mock_get.return_value = mock_resp
try:
get_temperature("Berlin")
except Exception as exc:
assert "Error" in str(exc)Mocking Databases
You generally do not want to mock every SQL call. That makes tests fragile and "fake".
Better common approaches:
- Use a real database in tests (covered in “Testing Databases” chapter)
- Or inject a repository interface and mock that
Example service:
# services/users.py
def get_user_profile(user_repo, user_id):
user = user_repo.get_by_id(user_id)
if user is None:
raise ValueError("User not found")
return {"id": user.id, "email": user.email}Test:
from unittest.mock import Mock
from services.users import get_user_profile
def test_get_user_profile_found():
fake_repo = Mock()
fake_user = Mock(id=1, email="test@example.com")
fake_repo.get_by_id.return_value = fake_user
profile = get_user_profile(fake_repo, 1)
assert profile == {"id": 1, "email": "test@example.com"}
fake_repo.get_by_id.assert_called_once_with(1)
def test_get_user_profile_missing():
fake_repo = Mock()
fake_repo.get_by_id.return_value = None
try:
get_user_profile(fake_repo, 1)
except ValueError as exc:
assert "User not found" in str(exc)Mocking Time, Randomness, and Environment
Backend logic often depends on:
- Current time
- Random tokens
- Environment variables
You want stable, repeatable tests, so you fix these values via mocking.
Time
# utils/time_utils.py
from datetime import datetime, timezone
def get_current_utc_hour():
return datetime.now(timezone.utc).hourTest:
from unittest.mock import patch
from datetime import datetime, timezone
from utils.time_utils import get_current_utc_hour
def test_get_current_utc_hour():
fake_now = datetime(2020, 1, 1, 15, 0, 0, tzinfo=timezone.utc)
with patch("utils.time_utils.datetime") as mock_datetime:
mock_datetime.now.return_value = fake_now
mock_datetime.timezone = timezone
assert get_current_utc_hour() == 15Random
# utils/token.py
import secrets
def generate_token():
return secrets.token_hex(16)Test:
from unittest.mock import patch
from utils.token import generate_token
def test_generate_token():
with patch("utils.token.secrets.token_hex", return_value="abc123"):
assert generate_token() == "abc123"Environment Variables
# config.py
import os
def get_db_url():
return os.environ["DB_URL"]Test:
from unittest.mock import patch
from config import get_db_url
def test_get_db_url():
with patch.dict("os.environ", {"DB_URL": "postgres://test"}):
assert get_db_url() == "postgres://test"Mocking in Asynchronous Code
With async code (for example FastAPI), you often need AsyncMock (Python 3.8+).
Example:
# async_client.py
import httpx
async def fetch_data():
async with httpx.AsyncClient() as client:
resp = await client.get("https://example.com")
resp.raise_for_status()
return resp.json()Test:
import pytest
from unittest.mock import AsyncMock, patch
from async_client import fetch_data
@pytest.mark.asyncio
async def test_fetch_data():
mock_client = AsyncMock()
mock_resp = AsyncMock()
mock_resp.json.return_value = {"ok": True}
mock_resp.raise_for_status.return_value = None
mock_client.get.return_value = mock_resp
with patch("async_client.httpx.AsyncClient", return_value=mock_client):
result = await fetch_data()
assert result == {"ok": True}
mock_client.get.assert_awaited_once_with("https://example.com")Common Mocking Pitfalls
Patching the Wrong Place
The most frequent mistake is:
@patch("external_email.EmailClient") # WRONGwhen the code does:
from external_email import EmailClient
and uses EmailClient inside user_service.py.
You must patch where it is looked up:
@patch("user_service.EmailClient") # CORRECTRule: Patch where the object is used, not where it is defined.
Over-Mocking
If you are mocking:
- Two or more functions within your module
- Or private helper functions
- Or “simple” Python types
you might be testing against implementation details.
Better:
- Extract dependencies into parameters or simple interfaces
- Use simple fake implementations instead of heavy mocks
Not Resetting State
Mocks can keep state across tests if reused.
With pytest, every test should get fresh mocks via:
- Local
Mock()creation patchused within the test- Or fixtures that set up mocks per test
Avoid global, reused mocks unless you carefully reset them in a fixture.
Fakes vs Mocks
Sometimes a fake implementation is easier and more reliable than mocks.
Example: An in‑memory repository:
class InMemoryUserRepo:
def __init__(self):
self._users = {}
def save(self, user):
self._users[user.id] = user
def get_by_id(self, user_id):
return self._users.get(user_id)
You can use this real class in tests instead of a Mock. It behaves like a simple database and keeps tests readable.
Use fakes when:
- Behavior is simple
- You need something that “actually works” but without external systems
Use mocks when:
- Behavior is complex or depends on network, time, environment
- You want to assert exact calls and parameters
Summary
- Mocking replaces real dependencies with controllable stand‑ins.
- Use
Mock,AsyncMock, andpatch(ormockerinpytest) to control and inspect interactions. - Mock external boundaries like HTTP, email, time, randomness, and environment variables.
- Prefer fakes or real test databases for complex I/O when possible, and avoid over‑mocking.
- Always patch where the dependency is used, not where it is defined.
Views: 6
KAHIBARO