unittest.mock replaces collaborators during a test and records their use. It helps isolate clocks, gateways, and external clients, but excessive mocking ties a suite to implementation details.

Patch the lookup namespace

If service.py uses from gateway import send, patch service.send, because that is where the function is looked up:

from unittest.mock import patch

from app.service import process


@patch("app.service.send", autospec=True)
def test_process_sends_event(send_mock) -> None:
    send_mock.return_value = {"id": "evt-7"}

    result = process({"order_id": 7})

    assert result == "evt-7"
    send_mock.assert_called_once_with({"order_id": 7})

autospec=True catches incompatible arguments. Use a context manager when patching only one section and let cleanup restore the original automatically.

Returns, failures, and sequences

return_value models success; side_effect can raise an exception or yield successive results. Cover only errors that the application actually handles. A mock accepting every attribute may hide typos, so a spec is usually safer.

Mock, MagicMock, and contracts

Mock creates attributes on demand and records calls. MagicMock also supplies language protocol methods such as __enter__, __iter__, and __len__. Use it when the collaborator genuinely participates in such a protocol, not merely as a convenient default. Unintended magic methods can let unrealistic test code pass.

Pass a class or instance as spec to limit available attributes. spec_set is stricter and also prevents assigning unknown names. create_autospec() builds a mock that follows the original signature:

from unittest.mock import create_autospec


class Gateway:
    def charge(self, order_id: int, amount: int) -> str:
        ...


gateway = create_autospec(Gateway, instance=True, spec_set=True)
gateway.charge.return_value = "pay-42"

assert gateway.charge(42, 1990) == "pay-42"
gateway.charge.assert_called_once_with(42, 1990)

This catches misspelled attributes and many signature changes. It does not execute internal validation or enforce annotated types. An integration test is still needed to prove that production code communicates correctly with the real implementation.

Model results and failures with side_effect

An exception assigned to side_effect tests an error path without causing a real external failure. An iterable provides successive outcomes and raises StopIteration when exhausted:

from unittest.mock import Mock

fetch = Mock(side_effect=[TimeoutError, {"status": "ok"}])

try:
    fetch()
except TimeoutError:
    pass

assert fetch() == {"status": "ok"}
assert fetch.call_count == 2

A function used as side_effect can calculate a result from the arguments. Keep it small. If it reproduces the entire dependency, the test maintains a second implementation that may repeat the same mistake or silently diverge.

wraps=real_object records calls while delegating execution. This creates a partial spy, not isolation. Real effects remain, so do not accidentally wrap network clients, disk writes, or uncontrolled clocks.

Assert interactions without coupling to internals

assert_called_once_with() expresses a single interaction contract clearly. For several calls, compare call_args_list with call objects:

from unittest.mock import Mock, call

publish = Mock()
publish("order.created", {"id": 1})
publish("order.created", {"id": 2})

assert publish.call_args_list == [
    call("order.created", {"id": 1}),
    call("order.created", {"id": 2}),
]

If order is not contractual, use assert_has_calls(..., any_order=True) or compare an order-independent representation. Do not assert every helper call simply because it is observable. A refactor that preserves behavior should normally keep tests green.

ANY is useful for a deliberately variable argument such as a generated identifier. Limit it to that field and separately inspect important properties through call_args. Matching everything removes the test's ability to detect regressions.

Choose a patch lifetime

A decorator lasts for the full test function. A context manager gives a visibly narrow scope. patch.object() helps when the target object or class is already available. patch.dict() temporarily changes a mapping, including os.environ, and restores it:

import os
from unittest.mock import patch

with patch.dict(os.environ, {"MODE": "test"}, clear=False):
    assert os.environ["MODE"] == "test"

Avoid calling a patcher's start() without guaranteed cleanup. In unittest.TestCase, register patcher.stop with addCleanup() immediately. A patch leaking into another test creates order-dependent failures.

The lookup rule remains central. If app.service imported send directly, later replacing gateway.send does not change the reference already bound in app.service. Trace the import when an apparently valid patch has no effect.

Test asynchronous collaborators

AsyncMock represents an awaitable function. Current Python versions let patch() choose it automatically for async functions:

from unittest.mock import AsyncMock

fetch = AsyncMock(return_value={"id": 7})
result = await fetch(7)

assert result == {"id": 7}
fetch.assert_awaited_once_with(7)

Use assert_awaited*, not only assert_called*, when awaiting is part of the behavior. For an asynchronous context manager, configure __aenter__ and __aexit__ explicitly so the object received by async with is clear.

Mock stable architectural boundaries

Mocks work best at boundaries: clocks, ID generators, queues, payment gateways, or API clients. Inside the domain, real value objects and pure functions usually make clearer tests. An in-memory fake can model a repository consistently across several operations without dozens of mock settings.

Do not mock an entire third-party library. Wrap it behind a small interface owned by the application and mock that interface. Keep a smaller set of integration tests for serialization, authentication, timeouts, and real API changes. This balance avoids both an unnecessarily slow suite and a suite that merely confirms its own configuration.

Review each test

Ask whether the dependency truly needs isolation, whether patch targets the lookup namespace, and whether a spec protects the intended contract. Confirm that the test checks an observable result or effect, models only failures the application handles, and always restores patched state. Finally, exercise the real boundary in an appropriate integration layer.

Configure properties and chains sparingly

A property needs PropertyMock attached to the mock's type rather than its instance. Chained collaborators can be configured through expressions such as client.session().send.return_value, but a long chain often exposes an awkward production interface. Introduce a small adapter instead of teaching every test the internal shape of a third-party library.

For a genuine chained protocol, mock_calls and call.call_list() represent the complete sequence. Use them only when the sequence is contractual. If the final result is sufficient, a state assertion usually survives refactoring better.

Use sentinels for identity

sentinel creates unique, readable objects for arguments whose concrete value is irrelevant but whose identity must be preserved:

from unittest.mock import Mock, sentinel

store = Mock()
store(sentinel.connection)

store.assert_called_once_with(sentinel.connection)

Mocks keep references to mutable arguments. If production code changes a list after the call, call_args will expose the later value. When state at call time is contractual, copy the argument inside a side_effect or improve the boundary so it receives an immutable value.

Reset is not isolation

reset_mock() clears call history and normally keeps return_value and side_effect. It helps in one test with explicit phases, but sharing a mock across tests creates hidden state. Prefer a fresh instance per test and short-lived fixtures.

Avoid overly broad negative assertions too. assert_not_called() proves only that the selected mock received no call. It cannot establish that no external effect happened, especially if the wrong namespace was patched. Pair it with observable output and a spec that fails if code looks for an unexpected attribute.

Distinguish test doubles

A stub returns prepared data, a spy observes calls, a fake supplies a simplified working implementation, and a mock in the strict sense verifies interaction expectations. unittest.mock can construct several of these doubles. Naming the role makes the technique easier to choose.

Use real values for pure domain rules. An in-memory fake may express a repository with coherent operations. A spec-backed mock fits a mandatory event publication. For an external API, combine the mocked adapter with contract or integration tests so the boundary is not validated only against assumptions.

Before mocking, consider whether a pure function or a small fake communicates intent better. The pytest fixtures and monkeypatch guide shows alternatives, while RESPX is more expressive for HTTPX.

The official unittest.mock documentation, accessed July 28, 2026, covers patching, specs, call assertions, and async mocks. Prefer assertions about system output and effects; verify internal calls only when they are part of the contract.