Skip to content

Mocking and Stubbing

Types

Mock

A mock is a test double that records calls and allows assertions on how it was used. It verifies behaviour: was the method called? With what arguments? How many times?

from unittest.mock import MagicMock, patch

def test_sends_email_on_registration(user_service):
    with patch("services.email.EmailClient.send") as mock_send:
        user_service.register(email="a@b.com", password="secret")
        mock_send.assert_called_once_with(
            to="a@b.com",
            subject="Welcome",
        )

Use when: verifying that a side-effect was triggered (email, webhook, audit log).

Stub

A stub returns pre-defined responses without recording calls. It verifies state: given this input, does the system produce the right output?

from unittest.mock import patch

def test_returns_user_from_cache(user_service):
    cached_user = {"id": "1", "email": "a@b.com"}
    with patch("services.cache.CacheClient.get", return_value=cached_user):
        result = user_service.get_user("1")
    assert result["email"] == "a@b.com"

Use when: isolating external dependencies that return data (cache, DB, remote API).

Fake

A fake is a lightweight working implementation of a dependency, not a recording tool.

class InMemoryUserRepository:
    def __init__(self) -> None:
        self._store: dict[str, dict] = {}

    def save(self, user: dict) -> None:
        self._store[user["id"]] = user

    def get(self, user_id: str) -> dict | None:
        return self._store.get(user_id)

    def delete(self, user_id: str) -> None:
        self._store.pop(user_id, None)

Use when: replacing a DB or external API for a full test suite run without infrastructure.


Comparison

Type Records calls Returns data Has logic Use case
Mock Yes Configurable No Verify side-effects
Stub No Pre-defined No Isolate data sources
Fake No Computed Yes Replace infrastructure
Spy Yes Real Real Observe real behaviour

Use Cases

External Dependencies

Replace outbound HTTP calls:

import httpx
import pytest
from pytest_httpx import HTTPXMock

def test_calls_payment_gateway(httpx_mock: HTTPXMock, order_service):
    httpx_mock.add_response(
        method="POST",
        url="https://payments.example.com/charge",
        json={"status": "ok", "transaction_id": "tx-1"},
    )
    result = order_service.charge(amount=100)
    assert result["transaction_id"] == "tx-1"

Failure Simulation

Inject errors to test resilience:

from unittest.mock import patch
import httpx

def test_retries_on_503(order_service):
    call_count = 0

    def flaky_response(request):
        nonlocal call_count
        call_count += 1
        if call_count < 3:
            return httpx.Response(503)
        return httpx.Response(200, json={"status": "ok"})

    with patch("httpx.Client.post", side_effect=flaky_response):
        result = order_service.charge(amount=100)

    assert result["status"] == "ok"
    assert call_count == 3

Risks

Risk Description Mitigation
Over-mocking Tests pass but real system is broken Contract tests + integration tests
Mock drift Mock returns data real API no longer returns Consumer-driven contract tests
False confidence All mocked = no real wiring verified Integration layer with real dependencies
Mocking internals Testing implementation not behaviour Mock at the boundary (HTTP, DB), not internals

Decision Guide

Scenario Use
Verify email was sent Mock
Isolate DB call, return data Stub
Replace DB for full suite Fake
Simulate network failure Stub / HTTPXMock
Test real DB queries Testcontainers (real DB)