DevOps

Автоматический запуск тестов в CI

2 просмотров

Введение

Основной цикл статей про тестирование и тема CI/CD соединяются здесь напрямую — автоматический запуск тестов при каждом изменении кода — одна из главных практических ценностей CI, превращающая тесты из опциональной, легко забываемой практики в обязательную, технически принуждаемую часть процесса разработки.

Концепция

Тесты, запускаемые автоматически в CI (через ctest, интегрированный с CMake, аналогично основному циклу статей про тестирование), дают немедленную, объективную обратную связь о том, не нарушило ли конкретное изменение существующую функциональность — в сочетании с защищёнными ветками , требующими прохождения CI как условия слияния, это технически предотвращает попадание изменений, нарушающих существующие тесты, в основную ветку, независимо от того, заметил бы это конкретный разработчик самостоятельно или нет.

Пример кода

# Интеграция ctest в CI-пайплайн с детальным выводом при сбое
- name: Сборка с тестами
  run: |
    cmake -B build -S . -DBUILD_TESTING=ON
    cmake --build build

- name: Запуск тестов
  run: ctest --test-dir build --output-on-failure --parallel 4
  # --output-on-failure — подробный вывод ИМЕННО для упавших тестов (не для всех, что было бы избыточно)
  # --parallel 4 — параллельный запуск независимых тестов для ускорения общего времени выполнения
# Публикация результатов тестов в формате, понятном CI-платформе (JUnit XML — широко поддерживаемый формат)
- name: Запуск тестов с экспортом результатов
  run: ctest --test-dir build --output-junit test_results.xml

- name: Публикация результатов тестов
  uses: dorny/test-reporter@v1
  if: always() # выполняется ДАЖЕ если предыдущий шаг (тесты) завершился неудачей —
                 # важно увидеть детальный отчёт именно когда что-то пошло не так
  with:
    name: Test Results
    path: build/test_results.xml
    reporter: java-junit
# Покрытие кода тестами (code coverage) — дополнительная метрика качества тестирования
- name: Сборка с инструментацией для покрытия
  run: |
    cmake -B build -S . -DCMAKE_CXX_FLAGS="--coverage"
    cmake --build build

- name: Запуск тестов и генерация отчёта покрытия
  run: |
    ctest --test-dir build
    gcovr --root . --xml coverage.xml

Пояснения к коду

--output-on-failure — практичная настройка ctest, показывающая подробный вывод (что конкретно пошло не так) только для реально упавших тестов, а не загромождающая лог CI подробным выводом и для успешно прошедших тестов, что значительно облегчает диагностику именно при реальном сбое. if: always() для шага публикации результатов тестов — важная деталь: без этого условия шаг публикации отчёта о тестах (включая отчёт о том, какие именно тесты упали) сам по себе не выполнился бы, если предыдущий шаг (запуск тестов) завершился с ошибкой — что лишило бы команду именно той детальной информации о причине сбоя, которая особенно нужна в момент, когда что-то реально пошло не так. Генерация отчёта покрытия кода (gcovr) даёт дополнительную, количественную метрику — какая доля кода реально выполняется существующими тестами, что помогает выявить недостаточно покрытые тестами участки кода, хотя сама по себе высокая метрика покрытия не гарантирует качества тестов (тест может выполнить код без реальной, содержательной проверки его корректности).

Подводные камни

  • Отсутствие явного требования прохождения CI-тестов как условия слияния pull request (статья 444 про защищённые ветки) — настройка автоматического запуска тестов сама по себе бесполезна с точки зрения технического принуждения к качеству, если результат этого запуска не влияет на возможность слияния изменений; для реальной практической пользы прохождение тестов должно быть явно настроено как обязательное условие через настройки защиты ветки.
  • Игнорирование «мерцающих» (flaky) тестов, иногда падающих не из-за реальной проблемы в тестируемом коде, а из-за недетерминированности самого теста (зависимость от тайминга, порядка выполнения параллельных тестов, внешних ресурсов) — повторяющиеся, необъяснимые сбои таких тестов со временем приводят к тому, что команда начинает игнорировать (или автоматически перезапускать без анализа) красные результаты CI, что подрывает саму ценность автоматизированного тестирования; такие тесты нужно явно выявлять и либо исправлять причину недетерминированности, либо временно исключать из обязательного набора, явно фиксируя это как известную, временную проблему, требующую решения.
  • Запуск избыточно длительного, полного набора тестов при каждом, даже самом мелком изменении без разделения на быстрые тесты (выполняемые при каждом push) и более медленные, исчерпывающие тесты (например, выполняемые реже — раз в день, или только перед релизом) — для проектов с действительно большим, медленно выполняющимся набором тестов стоит продумать стратегию разделения, балансирующую быструю обратную связь для разработчиков с полнотой проверки перед финальным релизом.
  • Преследование высокого показателя покрытия кода тестами как самоцели, без анализа реального качества этих тестов — тест, формально «покрывающий» строку кода (просто выполняющий её), но не проверяющий никакого осмысленного, содержательного утверждения о её корректном поведении (без assert/QVERIFY/QCOMPARE, аналогично основному циклу про тестирование), даёт ложное чувство защищённости от регрессий при формально высоком показателе покрытия, но без реальной, практической пользы такого «теста».