Представьте, что TestMain запускает тесты, но CI всегда получает успешный статус. Какой результат выполнения нельзя терять?
TestMain должен сохранить и передать наружу код, возвращённый m.Run(). Именно этот код отражает итог выполнения тестов: при ошибке тестов он ненулевой; если его проигнорировать и завершить процесс успешно, CI может пометить сборку как успешную.
Обычные тесты Go запускаются через сгенерированный тестовый бинарник, который сам управляет жизненным циклом набора. Когда понадобились общие действия для пакета — подготовка ресурсов, запуск тестов и финальная очистка, — в testing появился механизм TestMain.
Он передаёт управление разработчику, поэтому ответственность за запуск набора и корректный код завершения оказывается в пользовательском коде. Это позволяет добавлять общую инфраструктуру, но создаёт риск случайно скрыть результат тестирования.
Если TestMain вызывает m.Run(), но затем завершает процесс с кодом успеха, отдельные тесты могут фактически упасть, а система сборки этого не увидит. Локальный вывод будет содержать ошибки, однако автоматический пайплайн может продолжить работу как будто проверка прошла.
Проблема особенно опасна при интеграции с CI, где код завершения процесса обычно является главным сигналом успеха или неуспеха шага. Дополнительный риск возникает при ручном управлении очисткой: немедленный выход может не дать выполниться отложенным действиям.
m.Run() запускает зарегистрированные тесты и возвращает целочисленный статус. После этого TestMain должен завершить тестовый процесс с тем же значением, обычно через os.Exit.
В этом примере ошибки тестов не теряются. Важно помнить, что os.Exit завершает процесс немедленно: функции, отложенные через defer в TestMain, перед выходом не выполняются.
Если нужна общая очистка, её следует выполнить явно до os.Exit, сохранив исходный статус. Очистка не должна без необходимости заменять статус тестов; иначе можно замаскировать первичную причину сбоя. Если сама очистка критична и тоже должна влиять на результат, это нужно определить отдельным правилом и реализовать явно.
Команда добавила TestMain для запуска локальной базы данных. После тестов функция выполняла очистку, но всегда завершалась успешным кодом. В результате часть тестов падала, а CI разрешал слияние изменений.
Рассматривались два варианта. Первый — отказаться от TestMain и перенести настройку в отдельные тесты: это уменьшало риск неправильного завершения, но дублировало инфраструктурный код. Второй — оставить TestMain, сохранить результат m.Run(), выполнить очистку, а затем завершить процесс с исходным статусом.
Выбрали второй вариант: он сохранил общую настройку и сделал сигнал CI достоверным. Дополнительно проверили, что очистка выполняется до завершения процесса и не скрывает ошибку тестов.
m.Run() и не вызывать os.Exit?Нет, полагаться на это не следует. TestMain получает управление до стандартного завершения тестового бинарника, поэтому код завершения нужно явно связать с результатом m.Run(). Иначе последующая логика может завершить процесс с неверным статусом.
Если после неуспешных тестов очистка завершилась успешно, процесс всё равно должен сообщить о провале тестов. Иначе инфраструктурная операция замаскирует дефект продукта. Статус очистки следует учитывать только по заранее определённому правилу, например если невозможность очистки сама делает проверку недостоверной.
defer после вызова os.Exit в TestMain?Отложенные функции не выполнятся, потому что os.Exit немедленно завершает процесс. Поэтому критическую очистку нельзя оставлять только в defer перед os.Exit; её нужно выполнить явно до выхода либо использовать другой жизненный цикл, который гарантирует нужную очистку.