Программирование PythonТестированиеPython-разработчик, работающий с асинхронными сервисами

В асинхронном тесте зависимостью является функция, которую код должен вызвать через await. Какой тип mock к...

В асинхронном тесте зависимостью является функция, которую код должен вызвать через await. Какой тип mock корректно подменит такую зависимость?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Используйте AsyncMock. Он возвращает awaitable-объект, поэтому результат его вызова можно передать в await, а сам факт ожидания проверить отдельно от факта вызова.

Обычный Mock по умолчанию возвращает произвольное значение, которое не обязано быть awaitable. Поэтому код с await обычно завершится ошибкой типов во время выполнения теста.

Исторический контекст

Обычные mock-объекты изначально предназначались для синхронного кода: вызов зависимости фиксировался, а её результат задавался обычным значением. С появлением асинхронных функций этого стало недостаточно, поскольку асинхронный вызов имеет дополнительный этап — ожидание результата.

AsyncMock решает именно эту проблему: он моделирует не только вызов функции, но и поведение возвращаемого awaitable-объекта.

Постановка проблемы

Предположим, production-код вызывает асинхронный клиент и ожидает его результат. Если подменить клиент обычным Mock, тест может упасть на await, хотя сама проверяемая логика корректна.

Обратная ошибка тоже опасна: если использовать AsyncMock для синхронной функции, код может получить coroutine вместо обычного результата. Такой тест либо сразу завершится ошибкой, либо скроет неверный контракт зависимости.

Важно различать два события: mock могли вызвать, но возвращённый awaitable могли не ожидать. Для асинхронного контракта обычно требуется проверять оба условия.

Подробное решение

AsyncMock следует применять, когда подменяемая зависимость объявлена как асинхронная функция или по контракту возвращает объект, который должен быть обработан через await.

from unittest.mock import AsyncMock async def load_name(client): return await client.fetch_name() async def test_load_name(): client = type("Client", (), {})() client.fetch_name = AsyncMock(return_value="Ada") result = await load_name(client) assert result == "Ada" client.fetch_name.assert_awaited_once_with()

Вызов AsyncMock регистрируется как вызов, а ожидание результата — как await. Поэтому assert_called_once_with проверяет только факт вызова и его аргументы, тогда как assert_awaited_once_with подтверждает, что возвращённый awaitable действительно был ожидаем ровно один раз с указанными аргументами.

Если асинхронная зависимость вызывается, но её результат намеренно не ожидается, проверка должна отражать именно такой контракт. Однако для обычного запроса к асинхронному клиенту отсутствие await чаще означает ошибку реализации, которую тест должен обнаружить.

При создании mock полезно сохранять исходный интерфейс зависимости через spec или autospec, если это соответствует используемому способу подмены. Это помогает обнаруживать неправильные имена и сигнатуры, но не заменяет проверку того, что вызов был именно ожидаем.

Ситуация из практики

Сервис отправляет асинхронный запрос в клиент уведомлений и возвращает идентификатор сообщения. В первом варианте разработчик использовал обычный Mock. Тест падал на await, поскольку mock возвращал строку вместо awaitable-объекта.

Второй вариант использовал AsyncMock, но проверял только assert_called_once_with. Такой тест подтверждал, что метод вызвали, но не гарантировал, что код дождался результата. При забытом await это могло привести к ложноположительной проверке вызова и предупреждению о неожидаемой coroutine.

Выбран AsyncMock с проверкой результата и assert_awaited_once_with. Это одновременно проверяет корректный тип подмены, аргументы и выполнение обязательного этапа ожидания.

Что кандидаты часто упускают

  1. Достаточно ли проверить assert_called_once_with для AsyncMock?

Нет. Эта проверка подтверждает только регистрацию вызова mock и совпадение аргументов. Она не доказывает, что возвращённый awaitable был передан в await; для этого нужна проверка assert_awaited_once_with или соответствующая проверка количества ожиданий.

  1. Что произойдёт, если AsyncMock использовать для синхронной функции?

Вызов вернёт awaitable-объект, а не обычный результат. Если production-код ожидает строку, число или другой синхронный объект, он получит объект coroutine-подобного поведения и может сломаться позже, например при сравнении или сериализации. Тип mock должен соответствовать контракту заменяемой зависимости, а не удобству теста.

  1. Почему тест с AsyncMock может проходить, даже если код не использует результат вызова?

Проверка assert_awaited_once_with подтверждает, что await произошёл, но сама по себе не доказывает, что возвращённое значение повлияло на результат тестируемой функции. Если результат игнорируется, тест может не заметить ошибку передачи данных дальше. Поэтому нужно отдельно проверять итоговое поведение системы, а проверки mock использовать для значимых взаимодействий с зависимостью.