Параллельные тесты в CI/CD: шардирование и изоляция
Тесты, которые локально проходят за 2 минуты, в CI вдруг занимают 25. Смотришь в лог — раннер тратит 8 минут на установку зависимостей, ещё 3 на сборку, и только потом 14 минут линейно гоняет 800 тест-кейсов по одному. Параллельный запуск тестов — это не просто --parallel флаг. Это правильное шардирование, управление shared state, изоляция баз данных и грамотная работа с кэшем. Наш опыт показывает: грамотная настройка сокращает время пайплайна в 3-5 раз, а экономия ресурсов раннеров достигает 40%. На одном из проектов с 1500 тестами удалось сократить время с 45 до 12 минут.
Почему параллельные тесты падают в CI?
Большинство тест-фреймворков поддерживают параллельный запуск из коробки. Jest documentation recommends using --shard for large test suites. RSpec через parallel_tests гем, pytest через pytest-xdist. Но просто включить их недостаточно.
Проблема shared state: тесты, которые пишут в одну базу данных, не могут безопасно выполняться параллельно. user_id = 1 в одном тесте вступает в конфликт с user_id = 1 в другом. Для баз данных решение — отдельная БД на каждый worker или сброс транзакции после каждого теста. В Rails — DatabaseCleaner с :transaction стратегией для параллельных тестов. В Django — --keepdb с изолированными схемами.
Проблема тест-порядка: некоторые тесты имеют побочные эффекты — создают файлы, меняют env-переменные, мокают глобальные синглтоны. При параллельном запуске эти побочные эффекты накладываются. Выявляется это неприятно: тест проходит при изолированном запуске и падает в CI при параллельном.
Как добиться равномерного шардирования?
Для крупных наборов тестов параллелизм внутри одного раннера недостаточен — нужно распределять тесты по нескольким раннерам. Это test sharding.
В GitLab CI используется параметр parallel для запуска нескольких копий джобы:
test:
parallel: 6 # запустит 6 копий джобы
script:
- bundle exec rspec --format progress \
$(bundle exec ruby scripts/split_tests.rb $CI_NODE_INDEX $CI_NODE_TOTAL)GitLab устанавливает CI_NODE_INDEX (0..5) и CI_NODE_TOTAL (6) для каждого шарда. Скрипт split_tests.rb делит список spec-файлов на равные части по индексу.
В GitHub Actions через matrix:
---
test:
strategy:
matrix:
shard: [1, 2, 3, 4, 5, 6]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run tests (shard ${{ matrix.shard }}/6)
run: |
npx jest --shard=${{ matrix.shard }}/6 --coverage
---
Jest поддерживает --shard=N/TOTAL нативно с версии 28. Не нужно писать скрипт разбивки — Jest сам делит тесты по хэшу имени файла.
Когда применять шардирование по времени?
Простое деление по количеству файлов даёт неравномерные шарды: один может содержать 5 долгих интеграционных тестов, другой — 50 быстрых unit. Правильный подход — делить по ожидаемому времени выполнения.
RSpec через rspec_junit_formatter сохраняет время каждого теста в JUnit XML. knapsack_pro или самописный скрипт читает историю времён и балансирует шарды так, чтобы все заканчивались примерно одновременно. Knapsack Pro делает это через облачный API — собирает статистику со всех раннеров и оптимизирует распределение с каждым запуском.
Для pytest — pytest-split с аналогичным подходом: pytest --splits 6 --group 1 --durations-path .test_durations.
Сравним подходы:
| Метод | Равномерность | Сложность внедрения | Динамика |
|---|---|---|---|
| По количеству файлов | Низкая | Низкая | Нет |
| По времени выполнения | Высокая | Средняя | Да (Knapsack) |
Шардирование по времени (Knapsack) даёт в 1.5-2 раза более равномерное распределение, чем по количеству файлов, что подтверждается на наших проектах.
Что делать, если тесты зависят от общего состояния?
Если тесты используют общие файлы, глобальные переменные или внешние сервисы, параллельный запуск приведёт к race condition. Решение — полная изоляция: каждый worker получает свои временные файлы (через Tempfile), свои env-переменные (через dotenv или конфигурацию CI). Для внешних сервисов — использование моков или отдельных контейнеров для каждого worker. В Kubernetes можно запускать каждый шард в отдельном поде с изолированными ресурсами.
Изоляция базы данных при параллельных тестах
В Rails с parallel_tests каждый worker получает свою БД: test, test2, test3... Создаются через rake parallel:create parallel:migrate. В database.yml:
test: database: myapp_test<%= ENV['TEST_ENV_NUMBER'] %> В Django при шардировании по раннерам — каждый раннер создаёт свою тестовую БД. Если используете PostgreSQL — убедитесь, что у CI-пользователя есть права CREATEDB, иначе manage.py test упадёт с permission denied to create database.
Отчёты о тестах и анализ медленных тестов
При параллельном запуске JUnit XML отчёты генерируются на каждом шарде отдельно. CI-система должна собрать их все:
# GitLab — artifacts из всех шардов мержатся автоматически artifacts: reports: junit: reports/rspec-*.xml CircleCI Test Analytics, Buildkite Test Analytics, GitHub Actions с dorny/test-reporter — все умеют агрегировать отчёты с нескольких шардов и строить исторические тренды по времени каждого теста. Это позволяет найти тесты, которые замедлились после конкретного коммита.
Сравнение поддержки параллельного тестирования в CI-системах
| CI-система | Параллельные джобы | Шардинг | Агрегация отчётов |
|---|---|---|---|
| GitLab CI | parallel: keyword |
Вручную или Knapsack | Автоматическая |
| GitHub Actions | matrix strategy | --shard или вручную |
Через сторонние action |
| CircleCI | parallelism key | Вручную | Test Analytics |
| Jenkins | parallel stages | Вручную | Плагины |
Типичные ошибки при настройке параллельных тестов
Развернуть список
- Забывают изолировать базу данных:
DatabaseCleanerне настроен на:transaction— тесты падают сduplicate key. - Не учитывают кэширование: каждый шард собирает зависимости заново — время не сокращается.
- Используют шардирование по количеству файлов без анализа времени — шарды сильно различаются.
- Не проверяют тесты на последовательный запуск: после параллельного прогона тесты могут оставлять мусор, влияющий на следующие запуски.
- Не настраивают агрегацию отчётов — теряют информацию о проваленных тестах.
Что входит в работу
Мы выполняем настройку под ключ: от аудита до внедрения. В результате вы получаете:
- Анализ текущего пайплайна и выявление узких мест.
- Аудит тестов на shared state с рекомендациями по исправлению.
- Выбор и реализацию стратегии шардирования (по файлам или по времени).
- Настройку изоляции баз данных под параллельные раннеры.
- Настройку агрегации отчётов и алертов по медленным тестам.
- Документацию по поддержке и дообучение команды.
Этапы работы
- Анализ текущего времени пайплайна — где именно потеря времени, узкие места.
- Аудит тестов на наличие shared state проблем — прогон с
--random-seedв разных порядках. - Выбор стратегии шардирования — by count или by timing history.
- Настройка параллелизма в CI —
parallel:в GitLab, matrix в GitHub Actions. - Настройка изоляции БД на параллельных раннерах.
- Настройка агрегации JUnit отчётов.
- Измерение итогового времени пайплайна.
Срок: 1-3 дня в зависимости от сложности тестовой инфраструктуры. Стоимость рассчитывается индивидуально. Мы гарантируем прозрачный результат — вы увидите сокращение времени пайплайна уже после первого этапа. Если вы хотите ускорить свой пайплайн — закажите аудит уже сегодня. Свяжитесь с нами для оценки вашего проекта.







