Mock, MagicMock, patch, AsyncMock, and monkeypatching
coreintermediate`Mock` is an object that accepts any attribute access and any call, records what happened, and returns another `Mock`. `MagicMock` is the same with the dunder methods configured, so it supports `len()`, iteration, `in`, and context managers — which is why it is what `patch` gives you by default. `AsyncMock` returns a coroutine when called, so it stands in for anything you `await`. `patch` temporarily replaces an attribute for the duration of a test and puts the original back afterwards, and pytest's `monkeypatch` fixture does the same for attributes, dictionary entries and environment variables. The single rule that decides whether any of this works: **patch where the name is used, not where it is defined**.
Think of it as
A `Mock` says yes to everything, which is exactly what makes it useful and exactly what makes it dangerous. `mock.anything.at.all()` succeeds, so an assertion that is not quite an assertion — `charge.called_once_with(...)`, missing the `assert_` prefix — is just another mock call that returns a truthy object and passes. Modern `unittest.mock` catches the obvious version by raising for attributes starting with `assert`, which is precisely why the surviving mistake is the one that drops the prefix. `autospec=True` fixes the whole class by building the replacement from the real object's signature, so a wrong argument count or a nonexistent method raises the way it would in production; treat it as the default rather than an option. The patch-location rule follows from how imports work: `from services import charge` binds `charge` into the *importing* module's namespace, so patching `services.charge` afterwards rebinds a name your code is no longer reading. Patching `orders.views.charge` — the name at the place it is looked up — is what actually intercepts the call. And the choice between `patch` and `monkeypatch` is mostly ergonomic: `patch` is the standard-library tool with `autospec`, spec assertions and call recording, while `monkeypatch` is pytest's fixture and is nicer for `setenv`, `setitem` and `delattr` — both undo themselves at the end of the test, which is the property that matters.
What we're doing: Replace the one call that leaves the process, with a double that cannot silently accept a wrong call.
- 1–5
- The patch target is `orders.services`, because that is where the name is looked up. Patching `billing.stripe.charge_card` would rebind a name this code no longer reads.
- 9
- `assert_called_once_with` checks the arguments as well as the fact of the call — and `autospec=True` is what makes a wrong signature here fail rather than pass.
- 15–22
- The failure path asserts on *state*, not on the mock: the order is still a draft. Asserting only that the mock raised would test the mock rather than the code.
- 25–29
- `AsyncMock` because the collaborator is awaited; `await_count` rather than `call_count`, since a coroutine that is created but never awaited is a real bug this distinguishes.
- 32–34
- `monkeypatch.setenv` is cleaner than `patch.dict(os.environ)` and undoes itself, so no test can leak an environment variable into the next.
Why this works: Only the boundary call is replaced. Everything else — the order, the transaction, the status transition — is real, so the tests still fail if the code stops doing its job.
Patching where the function is defined
Wrong
Better
What you see: Either the assertion fails saying the mock was never called, or — much worse — the test passes while the *real* function runs and charges a card in a sandbox nobody is watching.
Why: `from billing.stripe import charge_card` binds the function object into `orders.services` at import time. Patching `billing.stripe.charge_card` afterwards replaces the attribute on the *defining* module, but `orders.services` still holds its own reference to the original object. The name your code actually reads is `orders.services.charge_card`, so that is the one to replace.
- Whole: @patch("orders.services.charge_card", autospec=True, return_value="ch_1")
- @patch — temporary, and self-undoing: Replaces the attribute for the duration of the test and restores it afterwards, whether the test passes, fails, or raises.
- "orders.services — the module where the name is LOOKED UP: Not where the function is defined. `from billing import charge_card` copies the reference into orders.services, so that is the namespace to patch.
- .charge_card" — the attribute being replaced: A string, resolved at call time — which is why a typo here silently patches nothing unless autospec catches it.
- autospec=True — the guardrail: Builds the double from the real signature, so a wrong argument count or a nonexistent method raises instead of quietly returning another Mock.
- return_value="ch_1" — what the call answers: Without it the call returns a Mock, which is truthy and often passes an assertion by accident. Use side_effect instead to raise or to vary per call.
Which double, and when
Together
Remember: `Mock` says yes to everything, which is why a mistyped assertion silently passes — use `autospec=True` as the default so a wrong signature or a fabricated method raises. `MagicMock` adds dunders, `AsyncMock` returns coroutines and is counted with `await_count`. Patch where the name is **used**, because `from x import y` copies the reference into the importing module. And prefer `monkeypatch` for environment variables and dictionary entries, where its teardown is cleaner than `patch.dict`.
See also: the test double vocabulary · what to mock and what not to · pytest django and fixtures

