Параллельные тесты в CI/CD: шардирование и изоляция

Когда тесты в CI занимают слишком много времени, это замедляет всю команду и затягивает релизы. Мы настраиваем параллельное выполнение тестов с правильным шардированием и изоляцией, чтобы ускорить пайплайны без потери стабильности. Наша команда реализует проект под ключ — от аудита до внедрения и последующей поддержки.
Часто задаваемые вопросы

Параллельные тесты в 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 с рекомендациями по исправлению.
  • Выбор и реализацию стратегии шардирования (по файлам или по времени).
  • Настройку изоляции баз данных под параллельные раннеры.
  • Настройку агрегации отчётов и алертов по медленным тестам.
  • Документацию по поддержке и дообучение команды.

Этапы работы

  1. Анализ текущего времени пайплайна — где именно потеря времени, узкие места.
  2. Аудит тестов на наличие shared state проблем — прогон с --random-seed в разных порядках.
  3. Выбор стратегии шардирования — by count или by timing history.
  4. Настройка параллелизма в CI — parallel: в GitLab, matrix в GitHub Actions.
  5. Настройка изоляции БД на параллельных раннерах.
  6. Настройка агрегации JUnit отчётов.
  7. Измерение итогового времени пайплайна.

Срок: 1-3 дня в зависимости от сложности тестовой инфраструктуры. Стоимость рассчитывается индивидуально. Мы гарантируем прозрачный результат — вы увидите сокращение времени пайплайна уже после первого этапа. Если вы хотите ускорить свой пайплайн — закажите аудит уже сегодня. Свяжитесь с нами для оценки вашего проекта.

Наши компетенции:

Последние работы

  • Разработка сайта компании B2B ADVANCE
    Разработка сайта компании B2B ADVANCE
    1542
  • Разработка веб-приложения для компании FEEDME
    Разработка веб-приложения для компании FEEDME
    1393
  • Разработка веб-сайта для компании БЕЛФИНГРУПП
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    1081
  • Разработка интернет магазина для компании FURNORO
    Разработка интернет магазина для компании FURNORO
    1368
  • Разработка логотипа компании B2B Advance
    Разработка логотипа компании B2B Advance
    815
  • Разработка веб-приложения для компании Enviok
    Разработка веб-приложения для компании Enviok
    1139