Isolation & Contracts

assert_called_with vs call_args_list in Python Mocks

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+ (AsyncMock and assert_awaited_* need 3.8; everything else is older).
  • The Mock call-recording model from deep dive into unittest.mock.

Solution

Every assertion reads one of four recorded fields. Match the assertion to the claim.

Python
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.

What each recorded field holds A stack of four Mock recording fields: call_args holding only the most recent call, call_args_list holding every call to this mock in order, mock_calls holding calls to children and their results with dotted names, and call_count as the integer number of calls. What each recorded field holds call_args the most recent call — overwritten every time call_args_list every call to this mock, in order mock_calls calls to children too, as call.child.method(...) call_count how many times this mock itself was called reset_mock() clears all four and leaves return_value and side_effect intact.
Every assertion helper is a thin wrapper over one of these four; reading them directly is always an option when no helper fits.

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.

Which assertion makes which claim A table of five call assertions - assert_called, assert_called_with, assert_called_once_with, assert_any_call and assert_has_calls - stating what each checks and what it deliberately does not check. Which assertion makes which claim Criterion Checks Does not check assert_called() at least one call arguments assert_called_with(...) the last call args call count or history assert_called_once_with(...) count is 1 and args nothing else assert_any_call(...) a match exists position or count assert_has_calls([...]) ordered subsequence that nothing else was called
The right-hand column is where tests go wrong: each helper is silent about something, and that silence is where bugs survive.

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.

Python
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:

Python
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:

Python
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.

Python
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.

Choosing an assertion from the claim A decision diagram choosing a call assertion by the claim being made: exactly one call with these arguments, a call somewhere in the history, an ordered sequence of calls, or no call at all. Choosing an assertion from the claim What exactly are you claiming happened? once, like this assert_called_once_with count and args together the honest default at some point assert_any_call position-independent busy graphs in this order assert_has_calls contiguous subsequence protocol order assert_not_called is the fourth, and the one most tests are missing.
Say the claim out loud first: the helper that matches the sentence is almost always the right one.

Edge cases and failure modes

  • assert_called_with on a never-called mock raises with a clear message, but assert_not_called is 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_autospec or Mock(spec=...) prevents this entirely — see autospec and strict mocking.
  • assert_has_calls matches a contiguous run within mock_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) and f(x=1) record differently and compare unequal, unless the mock was built with autospec, which normalises them against the real signature.
  • call_args.args and call_args.kwargs need Python 3.8+; older code indexes call_args[0] and call_args[1], which still works and is less readable.
  • Async mocks have parallel assertions. assert_awaited_with and await_args_list mirror 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.

← Back to Deep Dive into unittest.mock