Yeda AI Tips · #082

Español

Async Mocks: Assert Awaited, Not Called

Called but never awaited. It is the async test trap that passes green. When your code calls an async function but drops the await, Python creates the coroutine object and throws it away — the work never runs. And if that function is an AsyncMock in your test, assert_called_once() still passes, because calling is exactly what happened. The test is green; production is broken.

Why called ≠ awaited

Since Python 3.8, unittest.mock ships AsyncMock, which tracks two separate ledgers:

A forgotten await bumps the first ledger and never touches the second. Straight from the standard-library docs:

>>> mock = AsyncMock()
>>> coro = mock()          # called — but never awaited
>>> mock.called
True
>>> mock.assert_awaited()
AssertionError: Expected mock to have been awaited.

So assert_called_once() is the wrong question for an async dependency. It asks "did you build the coroutine?" — not "did the work run?"

The one-word fix

# Fragile: passes even if the await is missing
service.send_email.assert_called_once_with(user.id)

# Correct: fails loudly when the coroutine was never awaited
service.send_email.assert_awaited_once_with(user.id)

AsyncMock mirrors the whole call-assertion family on the await side, so the migration is mechanical:

Call-side assertionAwait-side equivalent
assert_called()assert_awaited()
assert_called_once()assert_awaited_once()
assert_called_with(...)assert_awaited_with(...)
assert_called_once_with(...)assert_awaited_once_with(...)
assert_any_call(...)assert_any_await(...)
assert_not_called()assert_not_awaited()
call_count / call_args_listawait_count / await_args_list

Rule of thumb: if the mock is an AsyncMock, every called in your assertions should read awaited. This matters double when an AI assistant writes your tests — models trained on years of sync-mock code reach for assert_called_once by default, so review generated async tests for exactly this substitution.

Why this bug loves AI-generated code

A missing await is a one-token diff that type-checks in plain Python, runs without an exception, and only surfaces as a RuntimeWarning: coroutine '...' was never awaited — printed after the fact, not raised. Assistants also produce it when converting sync code to async, because every call site needs a manual edit. Your tests are the net; assert_awaited_once is what makes the net actually catch.

Power-user moves

Resources

Building an AI feature? Yeda AI designs, audits, and ships production LLM systems.

Talk to us · Read the blog