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 к разработке поддерживаемых приложений. Вы разберётесь со структурой пакетов, объектной моделью, современной типизацией и автоматическими тестами, а затем соедините эти инструменты в небольшом приложении с хранением данных и логированием.