Асинхронность в Python
Теория: Тестирование асинхронного кода
Тестирование асинхронного кода требует инструментов, которые управляют циклом событий и корректно запускают корутины. Обычные тестовые фреймворки не умеют работать с async/await — попытка вызвать асинхронную функцию напрямую приведёт к ошибке. В Python используются два основных подхода: pytest-asyncio и встроенный IsolatedAsyncioTestCase.
pytest-asyncio и IsolatedAsyncioTestCase
pytest-asyncio
Плагин pytest-asyncio интегрируется с pytest, позволяя писать тесты в формате async def. Он автоматически создаёт event loop перед запуском каждого теста и очищает его после завершения.
Тесты пишутся как обычные корутины с маркировкой @pytest.mark.asyncio:
Плагин поддерживает асинхронные фикстуры для подготовки окружения:
Фикстуры с yield поддерживают асинхронный teardown — код после yield выполняется как финализатор:
IsolatedAsyncioTestCase
Встроенный класс IsolatedAsyncioTestCase из модуля unittest предоставляет аналогичную функциональность без сторонних зависимостей:
Класс поддерживает методы asyncSetUp() и asyncTearDown() для подготовки и очистки ресурсов:
Главное преимущество IsolatedAsyncioTestCase — каждому тесту создаётся новый цикл событий, который гарантированно закрывается после выполнения. Это предотвращает ситуации, когда фоновые корутины продолжают работу после завершения теста. Такая изоляция особенно важна при работе с объектами, привязанными к event loop: сессиями aiohttp, WebSocket-соединениями или пулами соединений к базе данных.
Моки и патчи для async-функций
При тестировании асинхронного кода часто требуется подменить внешние зависимости — API, базы данных или сетевые сервисы. Обычный Mock не работает с await, поэтому используется AsyncMock:
Здесь patch() перехватывает вызов fetch_user() и подменяет его асинхронным мок-объектом. Тест выполняется мгновенно, без реальных сетевых запросов. Можно проверять, как именно вызывался мок:
Метод assert_awaited() проверяет, что мок был вызван через await, а assert_awaited_once_with() — что он вызван один раз с указанными аргументами.
При тестировании кода с асинхронными контекстными менеджерами нужно подменять методы __aenter__() и __aexit__():
Такой подход позволяет тестировать HTTP-клиенты без реальных сетевых запросов. С IsolatedAsyncioTestCase моки работают аналогично:
Для тестирования обработки ошибок используется side_effect:
При мокировании классов, которые создаёт тестируемая функция, нужно подменять сам класс:
Если тестируемая логика зависит от времени выполнения, можно добавить задержку через side_effect:
Контроль и поиск «висячих» задач
Висячие задачи — это корутины, которые никогда не завершаются из-за забытого await, неправильной синхронизации или блокирующих вызовов. Они заполняют event loop, потребляют память и приводят к зависанию тестов или всего приложения.
Для обнаружения висячих задач используется asyncio.all_tasks(), который возвращает список всех активных задач:
Важно учитывать, что некоторые задачи создаются самим pytest-asyncio, поэтому сравнивать нужно только разницу до и после вызова тестируемого кода.
Если корутина должна завершиться за короткое время, используйте asyncio.wait_for():
Таймаут помогает быстро обнаружить зависание вместо бесконечного ожидания. Если подозревается блокирующая операция, можно проверить, продолжает ли работать цикл событий:
Если await asyncio.sleep(0) вызывает зависание, значит event loop заблокирован.
Задачи, созданные через create_task(), нужно либо awaited, либо отменять вручную:
Без явной отмены задача продолжит выполнение после завершения теста, что приведёт к ошибкам в следующих тестах.
При использовании asyncio.Event, Lock или других примитивов можно проверить корректность их работы:
В конце сложных тестов можно принудительно отменить все оставшиеся задачи:
Такой подход защищает тестовый набор от «утечек» задач между тестами.
Рекомендуемые программы
Завершено
0 / 10

