skip to content

How do AsyncMock's await assertions differ from call assertions like assert_called_once_with?

level: middleimportance: must knowfreq 55%

answer

  1. Two separate events, two separate tallies
  2. Being invoked is not being consumed
  3. call_count moves before await_count does
  4. await_count and await_args_list record the await
  5. assert_awaited_once_with checks the await ledger

basics

~20 s

An AsyncMock keeps two ledgers: call_count records when it was invoked and a coroutine created, await_count records when that coroutine was actually awaited. assert_awaited_once_with checks the await ledger, so it catches a missing await that call assertions happily pass.

solid answer

~40 s

Invoking an `AsyncMock` and awaiting it are two separate events, and the mock records them separately. `call_count`, `call_args_list` and `assert_called_once_with` describe the invocation, which for an async double merely creates a coroutine. `await_count`, `await_args`, `await_args_list` and the `assert_awaited`, `assert_awaited_once_with`, `assert_any_await`, `assert_has_awaits` and `assert_not_awaited` helpers describe the await, which is when `side_effect` runs and `return_value` is produced. They diverge whenever production code creates a coroutine it never consumes, or schedules it as a task that has not run yet — `call_count` is 1 while `await_count` is still 0. Since the await is the event that corresponds to the real side effect, assert on awaits for an async double; the await helpers exist only on `AsyncMock`.

code

python · 17 lines
python
import asyncio
from unittest.mock import AsyncMock

save = AsyncMock()

async def main():
    save("first")                              # called, never awaited
    await save("second")
    print(save.call_count, save.await_count)   # 2 1
    print(save.await_args_list)                # [call('second')]
    save.assert_awaited_once_with("second")    # passes
    try:
        save.assert_called_once_with("second")
    except AssertionError as exc:
        print("call assertion fails:", exc)

asyncio.run(main())

go deeper

for a junior

Recall that an AsyncMock counts calls and awaits separately, and that assert_awaited_once_with is the async counterpart of assert_called_once_with. Prefer the await version when the code under test awaits the double.

for a middle

Explain why the two ledgers diverge — the call builds a coroutine, the await consumes it — and name the await helpers, including await_args_list for reading the recorded arguments directly.

for a senior

Show the production judgement: a missing await is a real, silent bug class, so async doubles should be asserted on awaits, and negative claims should use assert_not_awaited rather than the weaker assert_not_called.

for a principal

Own the standard for async test doubles across the codebase, including whether unawaited-coroutine warnings fail the build and how review catches suites whose green result rests only on call assertions.

An `AsyncMock` keeps **two independent ledgers**, and confusing them is the classic way an async test passes while the code under test is broken. ### The two ledgers * **The call ledger** — `called`, `call_count`, `call_args`, `call_args_list`, `mock_calls` — records the moment the double is *invoked*. For an `AsyncMock` that moment produces a coroutine object and nothing else: no `side_effect` has run, no `return_value` has been handed over. * **The await ledger** — `await_count`, `await_args`, `await_args_list` — records the moment that coroutine object is actually *awaited*. Only then does the mock produce its result. The matching assertion families follow the same split. `assert_called_once_with`, `assert_any_call`, `assert_has_calls` and `assert_not_called` read the call ledger. `assert_awaited`, `assert_awaited_once`, `assert_awaited_with`, `assert_awaited_once_with`, `assert_any_await`, `assert_has_awaits` and `assert_not_awaited` read the await ledger. The await family exists only on `AsyncMock`; a `MagicMock` has no `await_count` at all. ### Why they diverge For ordinary straight-line `await double(x)` code the two ledgers move together, and the difference looks academic. It stops being academic in exactly the situations async tests are written for: * **A missing `await` in production code.** The call happens, the coroutine is dropped, the call assertions still pass. `assert_awaited_once_with` is the assertion that fails, and CPython also emits `RuntimeWarning: coroutine ... was never awaited` — which nobody reads. * **Fire-and-forget scheduling.** `asyncio.create_task(double(x))` calls the mock immediately but the await happens inside the task, only once the loop next runs it. Between the two, `call_count == 1` and `await_count == 0`. A test that asserts too early sees the call and misses the await; awaiting the task, or yielding with a zero-second sleep, closes the gap. This is also how you prove the task was ever scheduled to run at all. * **A coroutine stored and never consumed.** Code that builds a list of coroutines and forgets to gather them looks perfectly healthy through the call ledger. The divergence runs both ways, which is the detail interviewers probe. If a double is called twice but awaited once, `assert_awaited_once_with(...)` **passes** for the awaited arguments while `assert_called_once_with(...)` **fails** with "Called 2 times". Neither assertion is wrong; they are answering different questions. ### Which to assert For a coroutine-function double, **assert on awaits**. The await is the event that corresponds to the real side effect — the row written, the request sent — and the call is merely the object construction that precedes it. Keep call assertions for the cases where the two genuinely differ and you care about both: proving a coroutine was created and scheduled, or proving that a coroutine was created and deliberately *not* awaited. `assert_not_awaited` is the sharp tool on the negative side. "This code path must not touch the database" is a claim about awaits; asserting `assert_not_called` states something weaker, because a created-then-dropped coroutine would fail it while doing no harm. ### Reading the ledgers directly `await_args` holds the last await as a `call` object, and `await_args_list` holds all of them, so you can index and unpack them exactly like `call_args_list` when an assertion helper is too blunt — for example to check only the second positional argument of the third await, or to inspect the exception triple that a mocked `__aexit__` was awaited with. ### An await that raises still counts A `side_effect` that raises does not remove the await from the ledger: the await happened, the mock recorded it in `await_count` and `await_args_list`, and then the exception propagated. So a test that makes a double raise can still assert `assert_awaited_once_with(...)` afterwards to prove the failing call carried the arguments it should have. The same holds for the call ledger and a synchronous mock. ### Matching arguments is a separate axis Do not conflate *which* ledger an assertion reads with *how strictly* it matches. `assert_awaited` checks only that some await happened; `assert_awaited_once` adds "exactly one"; the `_with` variants add argument equality, and `assert_has_awaits` checks a sequence, optionally allowing other awaits between the ones listed. Picking the ledger is about the event; picking the helper is about how tightly you want to pin the arguments and the count, and the loosest assertion that still fails on the bug you care about is usually the one that survives refactoring. ### Version notes The await ledger and every `assert_awaited*` helper arrived with `AsyncMock` in Python 3.8 and are unchanged through 3.14. One old habit is worth naming: `assert_*` helpers are ordinary attributes, so a misspelled one on a mock without a spec silently creates a child mock and passes — which is why the `await_count` integer is a useful sanity check when an assertion looks suspiciously green.

  • Can assert_awaited_once_with pass while assert_called_once_with fails on the same mock?
    Yes. If the double is invoked twice but only one of the coroutines is awaited, the await ledger holds a single entry and its assertion passes, while the call ledger holds two and `assert_called_once_with` fails with `Called 2 times`. Neither is wrong; they answer different questions, which is why you pick the ledger matching the behaviour you mean to pin down.
  • Why is assert_not_awaited a stronger negative claim than assert_not_called?
    `assert_not_called` fails even when the coroutine was created and immediately dropped, which did nothing. `assert_not_awaited` states the claim you actually mean — the effect never happened — and it stays true for code that legitimately builds a coroutine to hand elsewhere. For 'this path must not touch that dependency', assert on the await.
  • Your assertion on await_count is 0 for a coroutine you scheduled with asyncio.create_task. Why?
    Creating the task calls the mock immediately, but the await runs inside the task, which needs the loop to get a turn. Between scheduling and that turn, `call_count` is 1 and `await_count` is 0. Await the task, or yield to the loop once, before asserting.

The call ledger is the stack of order slips written at the counter; the await ledger is the stack of meals actually collected. A slip written and abandoned looks like business until you count the second stack.

saying these in an interview costs you the question

  • Treats call_count and await_count as the same number
  • Asserts only on calls for a coroutine-function double
  • Thinks MagicMock also records await_count
  • Believes a missing await always raises an error
  • Asserts on a scheduled task before the loop runs it

context