KAHIBARO
Discord Login Register

19.7. Mocking

Why Mocking Matters

When you test backend code, your functions often talk to things outside your codebase:

Those “external” things are:

Mocking lets you replace those real dependencies with fake objects that:

So you can:

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:

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:

python
def send_welcome_email(email_client, user_email):
    email_client.send(
        to=user_email,
        subject="Welcome",
        body="Hello!"
    )
    return True

For a test you do not want to send real emails. Instead:

python
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.

TermWhat it doesTypical use
DummyPassed but never usedJust to satisfy a function signature
StubReturns fixed data, no assertionsMaking code think it got a response
SpyRecords how it was usedVerifying calls and arguments
MockCan stub behavior and verify callsCommon general-purpose fake
FakeSimple working implementation, not production‑readyIn‑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:

Avoid or limit mocking when:

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:

python
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:

python
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:
        pass

Useful attributes and methods:

FeatureExamplePurpose
Set return valuemock.method.return_value = 42Simple return
Set side effect (exception)mock.method.side_effect = ValueError()Simulate errors
Record call countmock.method.call_countHow many times called
Check exact callmock.method.assert_called_once_with(...)Verify interaction
Get all callsmock.method.call_args_listDetailed inspection

Patching with `unittest.mock.patch`

Sometimes you need to replace where the dependency is used, not where it is defined.

Imagine a module:

python
# 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:

python
# 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:

Using `patch` as a Context Manager

python
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:

python
with patch("my_module.time.time") as mock_time:
    mock_time.return_value = 1234567890

Mocking in `pytest` with `mocker`

If you use pytest, the pytest-mock plugin gives a mocker fixture that is convenient.

Example:

python
# 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:

python
# 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:

python
# 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:

python
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:

To simulate errors:

python
@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:

Example service:

python
# 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:

python
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:

You want stable, repeatable tests, so you fix these values via mocking.

Time

python
# utils/time_utils.py
from datetime import datetime, timezone
def get_current_utc_hour():
    return datetime.now(timezone.utc).hour

Test:

python
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() == 15

Random

python
# utils/token.py
import secrets
def generate_token():
    return secrets.token_hex(16)

Test:

python
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

python
# config.py
import os
def get_db_url():
    return os.environ["DB_URL"]

Test:

python
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:

python
# 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:

python
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:

python
@patch("external_email.EmailClient")  # WRONG

when the code does:

python
from external_email import EmailClient

and uses EmailClient inside user_service.py.

You must patch where it is looked up:

python
@patch("user_service.EmailClient")  # CORRECT

Rule: Patch where the object is used, not where it is defined.

Over-Mocking

If you are mocking:

you might be testing against implementation details.

Better:

Not Resetting State

Mocks can keep state across tests if reused.

With pytest, every test should get fresh mocks via:

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:

python
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:

Use mocks when:

Summary

Views: 6

Comments

Please login to add a comment.

Don't have an account? Register now!