Паралельні тести в CI/CD: шардування та ізоляція
Тести, які локально проходять за 2 хвилини, у CI раптом займають 25. Дивишся в лог — раннер витрачає 8 хвилин на встановлення залежностей, ще 3 на збірку, і тільки потім 14 хвилин лінійно проганяє 800 тест-кейсів по одному. Паралельний запуск тестів — це не просто --parallel флаг. Це правильне шардування, керування shared state, ізоляція баз даних та грамотна робота з кешем. Наш досвід показує: грамотне налаштування скорочує час пайплайну в 3-5 разів, а економія ресурсів раннерів досягає 40%. На одному з проектів з 1500 тестами вдалося скоротити час з 45 до 12 хвилин.
Чому паралельні тести падають у CI?
Більшість тест-фреймворків підтримують паралельний запуск з коробки. Документація Jest рекомендує використовувати --shard для великих тестових наборів. 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 дні залежно від складності тестової інфраструктури. Вартість розраховується індивідуально. Ми гарантуємо прозорий результат — ви побачите скорочення часу пайплайну вже після першого етапу. Якщо ви хочете прискорити свій пайплайн — замовте аудит вже сьогодні. Зв'яжіться з нами для оцінки вашого проекту.







