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.0if using fixtures.- The target rules from where to patch.
Solution
# 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):
...
# 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")
# 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()
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
@patchcome before fixture arguments. Mixing the two makes the signature even harder to read; prefer fixtures or context managers. patch.multiplewithoutDEFAULT. 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
withblocks. Three levels of indentation for three patches hides the test body. Use the parenthesised form orExitStack. - Class-level decorators.
@patchon 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.
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.
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.
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.
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.
Related
- Patching Strategies for Complex Codebases — the wider patching model.
- Patching Class Attributes with patch.object — object-based patches that combine cleanly.
- Wiring Test Doubles Through a Factory Function — the alternative to patching many collaborators.
- Taming Autouse Fixtures in Large Suites — when a patch fixture should and should not be autouse.