Тесты — это способ общения с системой: через них она рассказывает своему автору, что у неё болит. Чтобы общение было лёгким, тесты должны запускаться быстро. В идеале должна быть возможность задать вопрос системе о состоянии конкретного участка, и получить моментальный ответ.

Но практика показывает, что наладить такое общение может оказаться на удивление нетривиальной задачей. Чем мощнее становится инфраструктура, тем больше становится тестов, и время полноценного запуска по-прежнему измеряется часами. А код растёт и изменяется всё быстрее из-за ИИ, поэтому проверять приходится всё чаще.

В той или иной форме механизм точечных перезапусков в тестовых фреймворках обычно есть. Но сделать так, чтобы этот механизм гладко работал в CI — задача гораздо более сложная.

В этой статье я запущу тестовую сюиту на стеке JUnit + Gradle + GitHub Actions, и опробую различные способы перезапуска конкретных тестов в ней.

Подготовка

Для экспериментов буду использовать пробную тестовую сюиту: каждый запуск даёт 639 результатов (включая параметризованные), в районе 10–15% тестов намеренно падают.

В GitHub Actions запуск этих тестов обеспечивает следующий воркфлоу (в папке .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 секунд на тест. У реальной тестовой сюиты, работающей с внешними зависимостями и ресурсами, среднее время выполнения может быть на порядок больше.

Итак, запуск прошёл, я открываю один из тестов, и пытаюсь понять, случайным был сбой или нет. Какие варианты перезапуска есть в моём распоряжении?

Перезапустить всю сюиту

Можно просто заново запустить упавшую джобу. GitHub Actions позволяет это сделать через API или на странице запуска:

Перезапуск джобы
Перезапуск джобы

Этот вариант терпимый, но неприятный: ответа придётся ждать ещё минут 7, за это время я переключусь на другую задачу, а потом нужно будет переключаться обратно.

Если тестов в 10 раз больше, ждать придётся час с лишним: уже больно, а не неприятно.

Если тестов в 100 раз больше, ради одного теста я перезапускать уже точно ничего не буду. Самое простое здесь — проигнорировать падение, а это значит, во-первых, риск пропустить реальный флаки, во-вторых, падение доверия к тестовой базе.

Дождаться следующего запуска

Можно дождаться следующего регулярного (ночного) прогона. Скорее всего, здесь результат будет тот же, что и в предыдущем пункте — я проигнорирую тест. В противном случае придётся заблокировать решение о мёрже на день.

Перезапустить тест локально

Упавший тест можно перезапустить локально на моей машине:

./gradlew test --tests 'io.demo.SearchFunctionalityTest.shouldSearchForDifferentProducts'

Здесь в кавычках — имя тестового класса, а затем тестового метода, который я хочу перезапустить.

На первый взгляд это самый «удобный» и быстрый способ. Но по факту это не так.

Во-первых, львиная доля флаков возникает из-за проблем с окружением — запуская тест в моём локальном окружении, я не увижу сбоев, вызванных этими проблемами, и буду гадать, почему у меня всё работает, а на CI — нет.

Во-вторых, результат этого запуска так и останется на моём компьютере, а в общей истории тест по-прежнему будет сломанным. Если спустя какое-то время кто-то другой (или даже я) посмотрит на эту историю, придётся снова делать перезапуск.

Перезапустить в специальном воркфлоу

Параллельно к основному вокрфлоу можно настроить отдельный, параметризованный, который будет запускать только те тесты, названия которых переданы ему параметром.

Параметр может выглядеть так:

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_dispatch запрашивать конкретные тесты. Но так делать не стоит.

Во-первых, в основном воркфлоу помимо тестов почти наверняка будет масса других вещей — линтеры, SonarQube, выгрузка артефактов, уведомления, и т.д., и т.п. Всё это пришлось бы пропускать if-ами, и это создаёт отдельный пучок проблем: if можно забыть; если нужно поменять условия запуска, их надо менять в каждой точке; и т.п.

Ещё важнее следующее соображение: прогон — это контроль качества. Почти пустой запуск с одним тестом может быть засчитан как зелёный свет для пулл-риквеста — даже когда основной запуск только что запретил слияние веток.

Наконец, даже если сделать такой единый вокрфлоу, тесты в перезапуске всё равно лежат отдельно от основного запуска, так что история фрагментируется.

Автоматически перезапускать только упавшие тесты

К счастью, многие инструменты позволяют перезапускать только упавшие тесты. В некоторых инструментах сборки (SBT, Rake) есть возможность запустить только упавшие в прошлом запуске тесты. Такую же возможность предоставляет, например, PyTest (и для этого есть отдельный GitHub Action). А Allure 3 позволяет перезапускать все упавшие тесты автоматически, если это позволяет фреймворк.

В Gradle по умолчанию возможности перезапуска упавших тестов нет — но её можно включить через отдельный плагин.

Наконец, если как следует засучить рукава, можно настроить воркфлоу, который будет выгружать XML из JUnit и, опираясь на эти данные, выполнять при перезапусках только упавшие тесты — именно так я провёл вчерашний вечер с нейросеткой. Полезность этого упраженения сомнительная, но стало окончательно ясно, что задача нетривиальная.

В конечном итоге, автоматический перезапуск и целевой перезапуск упавших тестов — это самое близкое к тому, чего я добивался. Но всё равно не то: если я подозреваю, что зелёный тест — это ложно-положительный результат, перезапустить его отдельно я всё равно не смогу. Для действительно целевого перезапуска в рамках CI, который при этом оставался бы внутри общей истории тестов и не нарушал ворота качества, нужно обращаться к коммерческим TMS.

Выводы

История автоматизированного тестирования — это вечный спор, как между ядром и бронёй: мощность и возможности инфраструктуры играют в догонялки с постоянно растущим количеством кода, который нужно проверять. На каком-то этапе кода становилось больше из-за того, что учащались релизы; сейчас его становится больше, потому что программисты вооружились копилотами.

Чтобы через тесты систему действительно можно было расспросить о её состоянии, нужны специальные инструменты. В чистом виде целевой перезапуск возможен только с TMS. Стек JUnit + Gradle + GitHub Actions позволяет:

  • перезапускать целиком вокрфлоу GitHub

  • перезапускать локально отдельные тесты JUnit

  • перезапускать отдельные тесты в параметризованном воркфлоу

  • автоматически перезапускать только упавшие тесты через плагин Gradle

Надеюсь, ваш диалог с тестами будет продуктивным!

Комментарии (0)