Короткий ответ
Тесты стоит разделить на быстрый этап (unit-тесты, линтеры) и более медленный этап (интеграционные тесты с реальной БД), запускать их параллельно по нескольким воркерам (pytest-xdist) и настраивать кеширование зависимостей, чтобы обратная связь разработчику приходила как можно быстрее при сохранении достаточного покрытия перед мержем.
Как это работает подробнее
pytest-xdist распределяет тесты по нескольким процессам/ядрам (pytest -n auto), сокращая общее время прогона на многоядерных раннерах. Разделение на job'ы в CI-пайплайне (unit → integration → e2e) с ранним завершением (fail fast) на первом же упавшем быстром этапе экономит время и вычислительные ресурсы.
Пример кода
testing_example_320.yaml
jobs:
unit-tests:
steps:
- run: pytest tests/unit -n auto --maxfail=1
integration-tests:
needs: unit-tests
services:
postgres:
image: postgres:16
steps:
- run: pytest tests/integration -n 4Что использовать на практике
- Практика. Важно учитывать изоляцию при параллельном запуске: каждая параллельная группа тестов с реальной БД должна использовать собственную схему/базу или уникальный namespace, иначе параллельные воркеры будут конфликтовать за одни и те же данные. Кеширование зависимостей (pip/poetry) и Docker-слоёв ускоряет повторные прогоны.
Подводные камни
- Риск. Запускать все тесты, включая медленные e2e, на каждый push без разделения на этапы, из-за чего обратная связь занимает слишком много времени и разработчики реже проверяют CI перед мержем в основную ветку. Включать параллелизацию без обеспечения изоляции данных между воркерами, что проявляется как случайные, труднообъяснимые падения именно в CI, но не локально.