Тесты красные, job зелёная: JUnit-отчёт и код завершения тестовой команды

В merge request видны два упавших теста, однако pipeline зелёный и разрешает релиз. Это не обязательно ошибка разбора XML. JUnit-отчёт описывает результаты для интерфейса, а статус job определяется выполнением script и политикой allow_failure. Корректный отчёт может соседствовать с ошибочно возвращённым нулевым кодом.

Чаще всего код тестовой команды теряется в shell: после неё выполняется успешный echo, ошибка подавляется через || true или вывод передаётся в tee без обработки статуса первого процесса. Исправление начинается с реальной команды запуска, а не с переименования junit.xml.

Самостоятельный пример с одним падающим тестом

Создайте tests/test_math.py и .gitlab-ci.yml. Pytest записывает XML и возвращает ненулевой код при проваленном assert. В YAML намеренно нет tee и маскирующих условий.

def test_sum():
    assert 2 + 2 == 4

def test_wrong_expectation():
    assert 2 + 2 == 5
tests:
  image: python:3.12-alpine
  script:
    - python -m pip install pytest==8.3.5
    - python -m pytest tests --junitxml=report.xml
  artifacts:
    when: always
    reports:
      junit: report.xml
    paths:
      - report.xml

Ожидается одна успешная и одна неуспешная проверка, failed у job и доступный отчёт. После замены 5 на 4 оба теста должны пройти, а статус стать успешным. Версия pytest зафиксирована для повторяемости примера; рабочий проект должен устанавливать согласованный набор из своего lock-файла.

JUnit-отчёт сам по себе не меняет статус job. Reports даёт GitLab структуру результатов, paths оставляет файл доступным для скачивания, when: always помогает сохранить диагностический файл после ошибки script. Но отсутствующий XML из-за сбоя до старта pytest не появится автоматически.

Найдём место потери статуса

Откройте итоговый script после include и extends. Найдите || true, set +e, блоки с несколькими командами и последнюю команду shell-скрипта. Если wrapper выполняет pytest, затем успешно печатает сообщение и завершается его статусом, CI получает успех. При необходимости сохраните результат сразу после тестов и завершите wrapper именно этим кодом.

set +e
python -m pytest tests --junitxml=report.xml
test_status=$?
set -e
printf 'pytest exit code: %s\n' "$test_status"
exit "$test_status"

Этот вариант нужен, когда после тестов выполняется дополнительная диагностика. Без такой необходимости проще оставить прямой вызов pytest из первого примера. Переменную надо присваивать немедленно: любая промежуточная команда изменит значение $?.

Почему tee требует отдельного решения

В POSIX shell статус pipeline обычно равен статусу последней команды. У pytest | tee tests.log это статус tee, который может успешно записать вывод упавших тестов. Bash с pipefail меняет правило, но для него Runner должен действительно запускать Bash. В Alpine по умолчанию нельзя предполагать Bash только потому, что пример найден для него.

Если нужна переносимая запись журнала, можно сначала направить вывод тестов в файл, сохранить код, затем вывести файл и завершиться сохранённым кодом. При этом исчезнет живой поток вывода, зато причина статуса станет явной. Выбирайте способ под фактический shell и требования к наблюдению.

Проверьте политику ошибки поверх команды

Даже правильный ненулевой код может не блокировать pipeline при allow_failure: true или подходящем allow_failure:exit_codes. Посмотрите эти поля в самой job и в совпавшем rule. Для обязательных тестов задайте поведение, при котором ошибка действительно запрещает дальнейшие этапы.

Не путайте «в отчёте ноль тестов» с успешной проверкой. Неверный путь, фильтр выбора или ошибка загрузки могут означать, что нужные тесты вообще не выполнялись. Pytest имеет разные коды для проваленных тестов, ошибок запуска и отсутствия собранных тестов; сохраняйте исходный код, пока процесс явно не определил допустимые исключения.

Проверяйте исправление намеренно падающим тестом в отдельной ветке. Должны совпасть результат теста, код процесса, статус job и поведение следующей стадии. Только такой отрицательный сценарий подтверждает, что зелёный pipeline действительно зависит от тестов, а не от последней успешной служебной команды.

Ещё по теме

Продолжить изучение

Хотите собрать проверяемый путь от коммита до релиза? Jobs, runners, артефакты и деплой — темы программы «GitLab CI/CD: автоматизация сборки и деплоя».

Продолжить на практике

  • GitLab CI/CD: автоматизация сборки и деплоя ↗

    От первого .gitlab-ci.yml до pipeline, который тестирует код, собирает образ и выкладывает приложение. Runners, artifacts, cache, registry, окружения и безопасный откат — на одном практическом проекте.