AsyncMock вызвали, но не дождались: проверяем await в тесте
Асинхронная функция должна отправить событие перед подтверждением операции. После рефакторинга await случайно исчезает. Вызов зависимости всё ещё происходит: создаётся coroutine, а обработчик сразу возвращает успех. Тест с assert_called_once проходит и создаёт впечатление, что отправка состоялась.
У AsyncMock вызов и ожидание — разные наблюдаемые действия. Вызванная mock-функция возвращает awaitable; её исполнение начинается при ожидании. Проверки assert_awaited и assert_awaited_once_with отражают именно второй этап. Для операции, от которой зависит результат, это более содержательный контракт, чем факт создания coroutine.
Вызов отличается от ожидания
В примере сервис получает асинхронную функцию отправки как зависимость. Тест не обращается к сети и не требует pytest-плагина для async: обычный тест запускает короткий async-сценарий через asyncio.run. Это удобно для самодостаточной проверки; в большом наборе можно использовать принятый в проекте runner асинхронных тестов.
import asyncio
from unittest.mock import AsyncMock
async def complete(sender) -> str:
await sender({"event": "completed"})
return "accepted"
def test_notification_is_awaited():
async def scenario():
sender = AsyncMock()
assert await complete(sender) == "accepted"
sender.assert_awaited_once_with({"event": "completed"})
asyncio.run(scenario())
Потерянный await и отказ отправки
Для запуска нужен Python 3.12 и pytest: python -m pip install pytest, затем python -m pytest -q test_notify.py. Начальный тест проходит. Уберите await перед sender: возвращаемое значение всё ещё равно accepted, но assert_awaited_once_with упадёт. Так тест обнаруживает именно потерянное ожидание, а не полагается на предупреждение о неиспользованной coroutine.
Проверьте также отказ зависимости. Передайте AsyncMock(side_effect=OSError("offline")) и определите, должен ли сервис вернуть отказ или поднять исключение. При обязательной отправке подтверждение не должно уходить раньше её результата. При намеренно фоновой отправке интерфейс нужно устроить иначе: создать управляемую задачу, сохранить ссылку и определить её завершение и обработку ошибки.
Не заменяйте каждую асинхронную деталь на AsyncMock автоматически. Если тест проверяет порядок нескольких действий, иногда проще маленькая настоящая async-функция со списком событий и Event. Mock удобен для проверки границы вызова и аргументов, но модель конкурентности он не создаёт. Возвращение готового значения может скрыть ситуацию, в которой реальная зависимость уступает управление.
Различайте контракт результата и контракт взаимодействия. Если отправка — обязательный эффект, проверка await и полезной нагрузки оправдана. Если функция лишь вычисляет значение, тестировать каждый внутренний await обычно не нужно. Сначала сформулируйте, что должно произойти до ответа вызывающему коду, и закрепите именно это. Тогда AsyncMock помогает найти поведенческий дефект, а не просто повторяет структуру реализации.
Следующий шаг — перенести этот приём из отдельного файла в поддерживаемое приложение. В курсе «Python для продвинутых: ООП, типизация и тестирование» структура пакетов, объектная модель, mypy и pytest соединяются в типизированном трекере задач с хранением данных и логированием. Можно отработать связь между интерфейсом, реализацией и регрессионным тестом, а затем применить её к своему проекту.
Посмотреть программу и практику курса.
Документация: официальный источник по теме.
Другие задачи по теме: Типы, тесты и поведение программы.
Продолжить на практике
- Python для продвинутых: ООП, типизация и тестирование ↗
Практический курс-мост от базового Python к разработке поддерживаемых приложений. Вы разберётесь со структурой пакетов, объектной моделью, современной типизацией и автоматическими тестами, а затем соедините эти инструменты в небольшом приложении с хранением данных и логированием.
