A test asserts client.post.assert_called_with(url, json=payload) and passes. The code under test called client.post four times: three with a malformed payload, and once — last — correctly. The assertion never had a chance to catch it, because assert_called_with compares against call_args, and call_args holds the most recent call only. Choosing the wrong assertion is the most common way a mock-based test proves less than its author believes.
Prerequisites
- Python 3.8+ (
AsyncMockandassert_awaited_*need 3.8; everything else is older). - The
Mockcall-recording model from deep dive into unittest.mock.
Solution
Every assertion reads one of four recorded fields. Match the assertion to the claim.
from unittest.mock import Mock, call, ANY
client = Mock()
client.post("/charges", json={"amount": 1})
client.post("/charges", json={"amount": 2})
client.post("/charges", json={"amount": 3})
# 1. LAST call only — passes even though two earlier calls were different.
client.post.assert_called_with("/charges", json={"amount": 3})
# 2. Exactly one call, with these arguments. Fails here: call_count is 3.
try:
client.post.assert_called_once_with("/charges", json={"amount": 3})
except AssertionError as exc:
print("count checked:", exc)
# 3. Somewhere in the history, in any position.
client.post.assert_any_call("/charges", json={"amount": 1})
# 4. This exact sequence, in this order.
client.post.assert_has_calls([
call("/charges", json={"amount": 1}),
call("/charges", json={"amount": 2}),
])
# 5. The raw history, when the assertion is more specific than any helper.
amounts = [c.kwargs["json"]["amount"] for c in client.post.call_args_list]
assert amounts == [1, 2, 3]
# 6. Partial matching: assert the url, ignore the payload entirely.
client.post.assert_any_call("/charges", json=ANY)
The distinction that matters most in review is between (1) and (2). assert_called_with makes no claim about how many calls happened; assert_called_once_with does. When the intent is "the code called this once, like so" — which it usually is — the second form is the honest one, and it costs four characters.
Why this works
Mock.__call__ appends a Call object to call_args_list, sets call_args to that same object, increments call_count, and propagates an entry into the parent's mock_calls with the child's name attached. assert_called_with compares its arguments to call_args; assert_any_call scans call_args_list; assert_has_calls looks for the expected sequence as a contiguous run inside mock_calls. The differences between the assertions are therefore differences in which of those structures they read — not in strictness of comparison, which is identical == on Call objects throughout.
Asserting across an object graph
When the code under test walks a chain — client.session.get(...) — the assertion target is easy to get wrong, because each attribute access creates a distinct child mock with its own history.
from unittest.mock import Mock, call
client = Mock()
client.session.get("/a")
client.session.post("/b", json={})
# The child's own history: only the get.
assert client.session.get.call_args_list == [call("/a")]
# The parent's mock_calls: both, with dotted names, in global order.
assert client.mock_calls == [
call.session.get("/a"),
call.session.post("/b", json={}),
]
mock_calls on the parent is the only structure that preserves ordering between different children, which makes it the right tool for "the code opened the transaction before writing" style assertions. It is also the noisiest: accessing client.session in the test itself does not record anything, but calling a child's return value does, and those entries appear as call.session.get().json().
For the common case of asserting that a specific interaction happened somewhere in a busy graph, filter the list rather than matching it whole:
posts = [c for c in client.mock_calls if c[0] == "session.post"]
assert len(posts) == 1
assert posts[0].kwargs["json"] == {}
That form survives the addition of unrelated calls, which a whole-list equality assertion does not — and a brittle whole-list assertion is one of the main reasons mock-heavy suites break on every refactor.
Matching arguments loosely
Exact-match assertions on large payloads are unreadable and fail for irrelevant reasons. Three tools narrow the claim to what matters.
unittest.mock.ANY compares equal to anything and nests inside structures, so a timestamp or a generated id can be ignored while the rest of the payload is pinned. For a positional wildcard the same object works. When more than a wildcard is needed — "any string starting with /charges" — a small custom matcher class with __eq__ is idiomatic and reads well at the call site:
class StartsWith:
def __init__(self, prefix): self.prefix = prefix
def __eq__(self, other): return isinstance(other, str) and other.startswith(self.prefix)
def __repr__(self): return f"StartsWith({self.prefix!r})" # shown in failure output
client.post.assert_any_call(StartsWith("/charges"), json=ANY)
The __repr__ is not optional in practice: without it the assertion failure prints the object's default representation and the diff becomes unreadable, which defeats the purpose of the matcher.
Writing assertions that survive refactoring
Interaction assertions are the most refactor-sensitive tests in a suite, because they encode how the code works rather than what it produces. Three rules keep them from becoming a tax on every change.
Assert the boundary, not the internals. A test for an order service should assert what reached the payment gateway, not that an internal helper was called with a particular tuple. The gateway call is a contract with another system; the helper call is an implementation detail that will move.
Prefer one specific assertion to three vague ones. assert_called_once_with(url, json=expected) states the whole claim in one line. The same claim spread across assert_called(), a call_count check and a call_args inspection is longer, weaker, and fails in three different places when the code changes.
Assert absence deliberately. assert_not_called() is a real assertion and often the most valuable one in the test: proving that the cache path did not hit the database is the entire point of a caching test. Mock-heavy suites tend to be full of positive assertions and empty of negative ones, which is why caching, short-circuiting and guard-clause bugs survive them.
def test_cache_hit_skips_the_database(cache, db):
cache.set("k", "v")
service = Service(cache=cache, db=db)
assert service.get("k") == "v"
db.query.assert_not_called() # the actual claim of this test
A fourth habit is worth adopting at review time: read every interaction assertion and ask what bug it would catch. If the answer is "a refactor", the assertion is measuring structure rather than behaviour, and the test will be deleted the first time it gets in someone's way.
Edge cases and failure modes
assert_called_withon a never-called mock raises with a clear message, butassert_not_calledis the explicit way to state that claim.- Typos in assertion names pass silently on a bare
Mock.mock.assert_called_ones_with(...)creates a child mock and calls it, asserting nothing.create_autospecorMock(spec=...)prevents this entirely — see autospec and strict mocking. assert_has_callsmatches a contiguous run withinmock_calls; interleaved unrelated calls make it fail even when the order is correct. Filter the list first when other calls are expected.- Keyword and positional forms are not interchangeable.
f(1)andf(x=1)record differently and compare unequal, unless the mock was built withautospec, which normalises them against the real signature. call_args.argsandcall_args.kwargsneed Python 3.8+; older code indexescall_args[0]andcall_args[1], which still works and is less readable.- Async mocks have parallel assertions.
assert_awaited_withandawait_args_listmirror the sync versions and are the ones that actually prove the coroutine was awaited rather than merely called.
Frequently Asked Questions
Does assert_called_with check every call?
No. It checks only the most recent call, because it compares against call_args, which Mock overwrites on each call. A test that passes with assert_called_with may still have made several earlier calls with wrong arguments.
What is the difference between assert_has_calls and assert_any_call?assert_any_call checks that one matching call exists anywhere in the history. assert_has_calls checks that a sequence of calls appears in order, contiguously by default within the recorded list, which makes it the stricter of the two.
Why does mock_calls contain entries my mock never received?
Because mock_calls records calls to child mocks and their return values too, using dotted names. A call to mock.session.get() appears in mock.mock_calls as call.session.get(), which is what makes it useful for asserting interaction order across an object graph.
How do I assert a call was made with only some arguments matching?
Use unittest.mock.ANY for the arguments you do not care about, or inspect call_args.kwargs directly and assert on the individual keys. ANY compares equal to anything and works inside nested structures.
Why does assert_called_once_with pass when the mock was called twice?
It does not — but assert_called_with does, and the two are easy to confuse at a glance. If a test that should be counting calls is passing, check which of the two names it actually uses; the difference is _once and it is the difference between checking the call count and ignoring it entirely.
Related
- Deep dive into unittest.mock — the recording model these assertions read.
- Autospec and strict mocking — the only reliable defence against a misspelled assertion that silently passes.
- Mock vs MagicMock vs AsyncMock — when to use each — the await-aware assertions on the async side.
- Resolving side_effect and return_value conflicts — why a mock returned something the assertions did not expect.
← Back to Deep Dive into unittest.mock