Logo Craft Homelab Docs Нейросети Хостинг Контакты
Перезапуск одного теста в JUnit + Gradle + GitHub Actions: пять способов и их цена Трендовые github проекты в нашем телеграм канале. Подпишись →
25 сентября 2026 г.

Упал один тест в 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 — только указанные тесты. Против этого три аргумента.

  1. Условия расползаются по файлу. В основном пайплайне кроме тестов обычно есть линтеры, SonarQube, выгрузка артефактов, уведомления. Каждый шаг придётся обернуть в if. Один забытый if — и точечный перезапуск шлёт уведомления или гоняет анализ. При изменении условий правка нужна во всех точках сразу.
  2. Ломаются ворота качества. Прогон с одним тестом формально зелёный. Если branch protection смотрит на статус этого workflow, почти пустой запуск может разрешить мёрж, хотя полный прогон минуту назад его запретил.
  3. История всё равно раздроблена. Результаты перезапуска лежат в отдельном запуске, и связь с исходным падением остаётся только в голове.

Способ 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 и умеет инициировать их точечно.