Isolation & Contracts

Stacking Multiple Patches Without Argument Confusion

Three stacked @patch decorators and a test signature of (self, mock_mail, mock_db, mock_clock) look tidy until someone reorders the decorators — or writes them in the order that seems natural. Decorators apply bottom-up, so the bottom decorator's mock is the first argument, and a test that assumes top-down configures mock_mail with the database's return value. Nothing fails at that point; the test simply stops testing what it claims to.

The confusion is mechanical and avoidable. Named context managers keep each patch beside the variable it binds, fixtures give commonly patched collaborators stable names, and ExitStack handles variable-length patch sets. The more interesting observation is that a test needing many patches is usually telling you something about the code rather than about the test.

The ordering bug deserves attention because of how it fails. A test whose mocks are swapped rarely errors; it runs, configures the wrong double, and then asserts on a double that was never touched by the code path in question. The assertion might pass vacuously — assert_not_called on the wrong mock is trivially true — or fail with a message that makes no sense until someone counts decorators. Either way, the test has quietly stopped meaning what its name says, which is the worst outcome a test can have.

Prerequisites

  • Python 3.10+ for parenthesised context managers; 3.8+ for everything else.
  • pytest >= 8.0 if using fixtures.
  • The target rules from where to patch.

Solution

Python
# Fragile: bottom-up argument order, easy to get backwards.
from unittest.mock import patch


@patch("myapp.orders.send_email")         # third argument
@patch("myapp.orders.OrderRepository")    # second argument
@patch("myapp.orders.utcnow")             # FIRST argument: closest to the function
def test_order_confirmation(mock_utcnow, mock_repo, mock_send):
    ...
Python
# Robust: each patch named where it is declared (Python 3.10+).
from unittest.mock import patch


def test_order_confirmation():
    with (
        patch("myapp.orders.utcnow", return_value=FIXED_NOW) as utcnow,
        patch("myapp.orders.OrderRepository", autospec=True) as repo_cls,
        patch("myapp.orders.send_email", autospec=True) as send_email,
    ):
        confirm_order("ord_1")

    send_email.assert_called_once()
    repo_cls.return_value.mark_confirmed.assert_called_once_with("ord_1")
Python
# Several attributes of one module in a single call.
from unittest.mock import DEFAULT, patch


def test_notifications_are_sent():
    with patch.multiple("myapp.notify", send_email=DEFAULT, send_sms=DEFAULT) as mocks:
        notify_customer("cus_1", "shipped")

    mocks["send_email"].assert_called_once()
    mocks["send_sms"].assert_not_called()
Why stacked decorators map to arguments bottom-up Three patch decorators are stacked above a test function. The decorator nearest the function wraps it first and prepends its mock as the first argument; the next decorator wraps that and prepends its mock before, so the topmost decorator's mock ends up last. The argument list therefore reads in reverse of the decorator list. Decorators read top-down; arguments arrive bottom-up @patch("…send_email") @patch("…OrderRepository") @patch("…utcnow") def test(…) arg 1: mock_utcnow arg 2: mock_repo arg 3: mock_send
The crossing arrows are the whole problem. A named with statement removes them by putting each name beside its target.

Why this works

A decorator wraps the function beneath it. The decorator nearest the function wraps it first, and its wrapper prepends that decorator's mock to the arguments. The next decorator up wraps the already-wrapped function and prepends its own mock in front, and so on. The topmost decorator's mock therefore ends up last, and the argument list reads in reverse of the decorator list.

A with statement has no such inversion. Each patch(...) as name binds its mock to exactly the name written beside it, in the order written, and the parenthesised form introduced in Python 3.10 makes a long list readable without nested indentation. patch.multiple goes further for several attributes of one target, returning a dictionary keyed by attribute name so there is no positional mapping at all.

The common thread is that every robust alternative binds a mock to a name rather than a position. Positional binding is what decorators force, and it is fragile because nothing connects the position to the target except the reader's ability to count upward. Name-based binding — as, dictionary keys, fixture names — makes the connection explicit in the code, where a reviewer can see it and a refactoring tool can follow it.

Edge cases and failure modes

  • Decorators plus pytest fixtures. Mock arguments from @patch come before fixture arguments. Mixing the two makes the signature even harder to read; prefer fixtures or context managers.
  • patch.multiple without DEFAULT. Passing a concrete value installs that value, not a mock, and no entry for it appears in the returned dictionary.
  • Autospec on many targets. Each autospecced patch imports and inspects its target, which adds up. For large patch sets, fixtures scoped once per test keep it manageable.
  • Nested with blocks. Three levels of indentation for three patches hides the test body. Use the parenthesised form or ExitStack.
  • Class-level decorators. @patch on a test class applies to every method, with the same bottom-up ordering per method — and only to methods whose names start with the test prefix.

Fixtures give patches names that last

When the same collaborators are patched across many tests, moving each patch into a fixture solves the ordering problem permanently and removes duplication. Each fixture has a name, tests request the ones they need, and pytest injects them by name rather than position.

Python
import pytest
from unittest.mock import patch


@pytest.fixture
def send_email():
    with patch("myapp.orders.send_email", autospec=True) as mock:
        yield mock


@pytest.fixture
def frozen_now():
    with patch("myapp.orders.utcnow", return_value=FIXED_NOW) as mock:
        yield mock


def test_confirmation_email_is_sent(send_email, frozen_now):
    confirm_order("ord_1")
    send_email.assert_called_once()

Argument order is now irrelevant — (frozen_now, send_email) works identically — and the fixture's name documents what is being replaced. The configuration each patch needs lives in one place, so a change to how the email sender is doubled happens once rather than in every test that patched it. Fixtures can also depend on each other, so a confirmed_order fixture that requests send_email and frozen_now composes a whole scenario from named parts — something stacked decorators cannot express at all without repeating every patch in every test.

Patches as named fixtures Two fixtures, send_email and frozen_now, each wrap one patch and yield its mock. A test requests them by name in any order, and pytest injects each by name rather than position, so the mapping between mock and target cannot be confused and each patch's configuration lives in one place. Injected by name, so order cannot matter send_email fixture patch + autospec, once frozen_now fixture fixed clock, once def test_x(frozen_now, send_email) any order · names match targets
Moving patches into fixtures also makes the patched collaborators discoverable: pytest --fixtures lists them with their docstrings.

A variable number of patches

Occasionally the set of patches is not fixed — a parametrised test that disables a different combination of feature flags per case, or a helper that neutralises every external integration listed in configuration. contextlib.ExitStack enters any number of context managers in a loop and exits them all, in reverse order, when its own block ends.

Python
import contextlib
from unittest.mock import patch


def test_every_integration_can_be_disabled(integrations):
    with contextlib.ExitStack() as stack:
        mocks = {
            name: stack.enter_context(patch(target, autospec=True))
            for name, target in integrations.items()
        }
        run_nightly_job()

    for name, mock in mocks.items():
        mock.assert_not_called(), name

The dictionary keyed by name keeps the same property as the named with statement: every mock is retrieved by what it replaces, never by position. ExitStack also guarantees that if entering the fourth patch fails — a typo in its target — the three already entered are unwound, so a broken test cannot leave global state patched for the rest of the session.

When the patch list is the bug

The mechanics above solve the ordering problem. They do not solve the underlying one, which is that a test needing six patches is testing code that reaches out to six collaborators on its own. Every patch is a dependency the code acquires by import rather than receiving from its caller, and every one is a place where the test has to know an implementation detail — the module path where the code looks the collaborator up.

The durable fix is to invert those dependencies. A function that takes its repository, clock and mailer as arguments needs no patches at all: the test passes doubles directly, the signature documents what the code depends on, and a refactor that moves the mailer to a different module breaks no tests. Converting one heavily patched function this way is usually a small change, and it tends to reveal that several of the patches were covering the same few collaborators under different names.

From many patches to injected collaborators On the left, a function imports six collaborators and its test needs six patches, each coupled to a module path. On the right, the same function receives its collaborators as parameters, the test passes doubles directly with no patching, and the signature documents every dependency. Six patches are six hidden dependencies reaches for collaborators imports repo, clock, mailer, sms, audit, metrics at module level test: six patches by module path any move breaks every test receives collaborators confirm(order_id, *, repo, clock, notifier) test: pass doubles, no patches signature documents dependencies
The right-hand version often has fewer dependencies too, because injecting them makes it obvious which ones overlap.

A useful threshold: three patches in a test is normal, four is worth a second look, and five or more is a design conversation. The approach to having it is laid out in dependency injection for testability, and the payoff is tests that stop breaking when files move. It is the rare refactor that makes both the production code and its tests shorter at the same time. Start with the test that has the most patches; it is usually the one that breaks most often.

Frequently Asked Questions

In what order are stacked @patch decorators passed to the test? Bottom-up. The decorator closest to the function is applied first and supplies the first mock argument. So @patch("a") above @patch("b") gives def test(mock_b, mock_a). Getting this backwards silently configures the wrong mock.

What is the cleanest way to apply several patches in one test? A parenthesised with statement (Python 3.10+) naming each mock with as, or patch.multiple for several attributes on one object. Both keep each patch next to its name, so the order cannot be confused.

How many patches is too many? When a test needs more than three or four, the code under test usually has too many hard-wired collaborators. Each patch is a dependency the code reaches for rather than receives. Injecting them removes the patches entirely and makes the dependencies visible in the signature.

← Back to Patching Strategies for Complex Codebases