Трендовые github проекты в нашем телеграм канале. Подпишись → Упал один тест в CI: как проверить его, не прогоняя всю сюиту
Сценарий знакомый: пайплайн покраснел, в отчёте один упавший тест, и нужно быстро понять — это реальная регрессия или флаки. Чем больше тестовая база, тем дороже этот вопрос. Ниже — эксперимент на стеке JUnit + Gradle + GitHub Actions: какие способы точечного перезапуска доступны из коробки и что каждый из них стоит по времени и по качеству истории прогонов.
Стенд
Тестовая сюита даёт 639 результатов с учётом параметризованных тестов, из них 10–15% падают намеренно. Запускает их обычный workflow в .github/workflows:
name: run-tests
on:
push:
workflow_dispatch:
inputs:
TEST_ENDPOINT:
description: "Endpoint for tests"
required: true
default: https://dev.github.com
TEST_BROWSER:
description: "Browser for tests"
required: true
default: chrome
permissions:
contents: write
jobs:
all-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Set up JDK 25
uses: actions/setup-java@v6
with:
distribution: temurin
java-version: "25"
- name: Build with Gradle
run: |
./gradlew clean test
env:
TEST_ENDPOINT: ${{ github.event.inputs.TEST_ENDPOINT }}
TEST_BROWSER: ${{ github.event.inputs.TEST_BROWSER }}
Полный прогон в CI занимает 7 минут 33 секунды, около 0,7 секунды на тест. Это синтетика: у сюиты, которая ходит во внешние сервисы и браузеры, среднее время на тест бывает на порядок больше.
Способ 1: перезапустить всю джобу
Кнопка Re-run на странице запуска или вызов через API GitHub. Работает без настройки, но ответ приходит через те же 7 минут. За это время успеваешь переключиться на другую задачу, а потом приходится возвращаться в контекст.
Масштабирование здесь линейное и болезненное:
- в 10 раз больше тестов — больше часа ожидания ради одного результата;
- в 100 раз больше — полный перезапуск ради одного теста никто делать не станет.
На больших сюитах падение просто игнорируют. Так пропускается настоящий флаки, а команда постепенно перестаёт доверять красным прогонам.
Способ 2: дождаться ночного прогона
Самый дешёвый по усилиям вариант. На практике он сводится к тому же игнорированию, либо решение о мёрже откладывается на сутки.
Способ 3: локальный запуск
Gradle умеет фильтровать тесты по имени класса и метода:
./gradlew test --tests 'io.demo.SearchFunctionalityTest.shouldSearchForDifferentProducts'
Выглядит как самый быстрый путь, но у него две проблемы.
Другое окружение. Большая часть флаков вызвана окружением: сетью, ресурсами раннера, внешними зависимостями. Локально эти условия не воспроизводятся, тест проходит, и остаётся гадать, почему в CI он красный.
Результат остаётся на ноутбуке. В общей истории тест по-прежнему числится упавшим. Коллега, который откроет этот прогон через неделю, увидит красный тест и будет разбираться заново.
Способ 4: отдельный параметризованный workflow
Рядом с основным workflow заводится второй, который принимает имя теста через workflow_dispatch:
on:
workflow_dispatch:
inputs:
TEST_NAME:
description: "Test to run (e.g. io.demo.AnalyticsTest or io.demo.AnalyticsTest.someTest)"
required: true
И команда запуска подхватывает этот параметр:
- name: Build with Gradle
run: |
./gradlew clean test --tests "${{ github.event.inputs.TEST_NAME }}"
env:
TEST_ENDPOINT: ${{ github.event.inputs.TEST_ENDPOINT }}
TEST_BROWSER: ${{ github.event.inputs.TEST_BROWSER }}
Теперь тест гоняется в окружении раннера, и ждать приходится старта джобы и одного теста. Копировать полное имя теста в форму GitHub неудобно, но это всё равно быстрее полного прогона.
Минус — фрагментация истории. Перезапуски живут в отдельном workflow, и при возврате к исходному прогону связь между «упал здесь» и «прошёл там» не видна.
Почему не стоит совмещать оба режима в одном файле
Напрашивается идея: один workflow, где push или pull request гоняет всю сюиту, а workflow_dispatch — только указанные тесты. Против этого три аргумента.
- Условия расползаются по файлу. В основном пайплайне кроме тестов обычно есть линтеры, SonarQube, выгрузка артефактов, уведомления. Каждый шаг придётся обернуть в
if. Один забытыйif— и точечный перезапуск шлёт уведомления или гоняет анализ. При изменении условий правка нужна во всех точках сразу. - Ломаются ворота качества. Прогон с одним тестом формально зелёный. Если branch protection смотрит на статус этого workflow, почти пустой запуск может разрешить мёрж, хотя полный прогон минуту назад его запретил.
- История всё равно раздроблена. Результаты перезапуска лежат в отдельном запуске, и связь с исходным падением остаётся только в голове.
Способ 5: автоматический перезапуск упавших
Многие инструменты умеют повторно запускать только то, что упало в прошлый раз:
- SBT и Rake — запуск тестов, упавших в предыдущем прогоне;
- PyTest — то же самое, плюс для него есть готовый GitHub Action;
- Allure 3 — автоматический перезапуск всех упавших тестов, если это поддерживает фреймворк.
В Gradle такой функции по умолчанию нет, она включается отдельным плагином.
Можно собрать решение и вручную: выгружать JUnit XML как артефакт, парсить из него список упавших и передавать их в --tests при повторном запуске. Это рабочая схема, но на практике она быстро обрастает деталями — параметризованные тесты, экранирование имён, передача артефактов между запусками — и становится отдельным проектом.
Автоматический ретрай ближе всего к нужному результату, но у него есть слепое пятно: он перезапускает только красные тесты. Если есть подозрение, что зелёный тест прошёл случайно и на деле ничего не проверяет, прогнать его отдельно этот механизм не даст.
Какой вариант выбирать
| Способ | Время ответа | Окружение CI | Общая история | Риск для ворот качества |
|---|---|---|---|---|
| Re-run джобы | полный прогон | да | да | нет |
| Ночной прогон | до суток | да | да | нет |
Локально --tests | один тест | нет | нет | нет |
| Отдельный workflow | старт раннера + один тест | да | фрагментирована | нет, если workflow отдельный |
| Ретрай упавших | время упавших | да | да | нет |
Практические правила для этого стека:
- Сюита до 10–15 минут — хватает обычного Re-run, настраивать что-то сверх этого нет смысла.
- Сюита на час и больше — подключить плагин ретрая для Gradle, чтобы флаки отсеивались в рамках того же запуска.
- Нужно проверить конкретный тест в CI-окружении — отдельный параметризованный workflow, строго в своём файле и без влияния на required checks.
- Локальный
--tests— для отладки логики самого теста; на вопрос «флаки или регрессия» он не отвечает.
Целевой перезапуск произвольного теста, красного или зелёного, в том же CI-окружении, с записью в общую историю и без обхода ворот качества, связка JUnit + Gradle + GitHub Actions сама по себе не даёт. Для этого нужна система управления тестированием (TMS), которая хранит историю отдельно от запусков CI и умеет инициировать их точечно.