Mocks asíncronos: verifica el await, no la llamada
Llamado pero nunca esperado con await. Es la trampa de los tests asíncronos que pasa en verde. Cuando tu código llama a una función async pero omite el await, Python crea el objeto corrutina y lo descarta — el trabajo nunca se ejecuta. Y si esa función es un AsyncMock en tu test, assert_called_once() igual pasa, porque llamar es exactamente lo que ocurrió. El test queda verde; producción queda rota.
Por qué llamado ≠ esperado
Desde Python 3.8, unittest.mock incluye AsyncMock, que lleva dos registros separados:
- Registro de llamadas —
call_count,called,assert_called_once(): se incrementa en el momento en que el mock es llamado, es decir, cuando se crea el objeto corrutina. - Registro de awaits —
await_count,await_args,assert_awaited_once(): se incrementa solo cuando esa corrutina es realmente esperada conawait.
Un await olvidado suma al primer registro y nunca toca el segundo. Directo de la documentación de la biblioteca estándar:
>>> mock = AsyncMock()
>>> coro = mock() # called — but never awaited
>>> mock.called
True
>>> mock.assert_awaited()
AssertionError: Expected mock to have been awaited.
Así que assert_called_once() es la pregunta equivocada para una dependencia asíncrona. Pregunta "¿construiste la corrutina?" — no "¿se ejecutó el trabajo?"
El arreglo de una palabra
# 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 replica toda la familia de aserciones de llamada del lado del await, así que la migración es mecánica:
| Aserción de llamada | Equivalente de await |
|---|---|
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_list | await_count / await_args_list |
Regla práctica: si el mock es un AsyncMock, cada called en tus aserciones debería decir awaited. Esto importa el doble cuando un asistente de IA escribe tus tests — los modelos entrenados con años de código de mocks síncronos usan assert_called_once por defecto, así que revisa los tests asíncronos generados buscando exactamente esta sustitución.
Por qué este bug adora el código generado por IA
Un await faltante es un diff de un token que compila en Python puro, corre sin excepción y solo aparece como un RuntimeWarning: coroutine '...' was never awaited — impreso después del hecho, no lanzado. Los asistentes también lo producen al convertir código síncrono a asíncrono, porque cada punto de llamada requiere una edición manual. Tus tests son la red; assert_awaited_once es lo que hace que la red de verdad atrape.
Trucos avanzados
- Convierte el warning en fallo. Python emite
RuntimeWarning: coroutine 'x' was never awaitedcuando una corrutina no esperada es recolectada por el garbage collector. En pytest, promuévelo: agregafilterwarnings = ["error::RuntimeWarning"]a tu configuración y elawaitfaltante hace fallar la suite incluso donde olvidaste la aserción. - Rastrea dónde se creó la corrutina. Ejecuta el loop en modo debug (
asyncio.run(main(), debug=True)) y el warning de "never awaited" incluye un traceback que apunta al punto exacto que omitió elawait. - Deja que autospec elija la clase de mock.
patch(..., autospec=True)ycreate_autospec()detectan funcionesasync defy producen unAsyncMockautomáticamente — y unMagicMockpara métodos síncronos de la misma clase — así los specs se mantienen honestos sin cableado manual. reset_mock()limpia ambos registros. Poneawait_counten cero y vacíaawait_args_listjunto con el lado de llamadas, así las aserciones por fase en un test largo quedan limpias.
Recursos
AsyncMock— documentación de unittest.mock (familia assert_awaited)- Referencia de
assert_awaited_once - Detectar corrutinas nunca esperadas — guía de desarrollo de asyncio
- Convertir warnings en errores — documentación de pytest
¿Construyendo una funcionalidad con IA? Yeda AI diseña, audita y entrega sistemas LLM de producción.