Что произойдёт с функциями, зарегистрированными через t.Cleanup, если тест завершится вызовом t.Fatal?
Функции, зарегистрированные через t.Cleanup, всё равно выполнятся после завершения теста, даже если тест остановлен через t.Fatal. Они запускаются в обратном порядке регистрации — по принципу LIFO.
t.Fatal немедленно прекращает выполнение текущего теста, но жизненный цикл теста завершается штатно, поэтому testing вызывает зарегистрированные функции очистки.
Обычный defer хорошо подходит для локальной очистки внутри одной функции, но становится неудобным, когда ресурс создаётся во вспомогательной функции или используется несколькими вложенными subtests. Вызов t.Cleanup связывает освобождение ресурса именно с жизненным циклом теста.
Такой подход решает проблему утечек временных файлов, соединений, изменённых настроек и других ресурсов при раннем завершении теста.
Если очистка выполняется только в конце основного тела теста, она может не сработать после t.Fatal: инструкции после него не выполняются. В результате временные файлы, глобальные настройки или mock-состояние могут остаться изменёнными и повлиять на следующие тесты.
Особенно опасно регистрировать очистку слишком поздно — после действия, которое само может завершить тест. Надёжнее регистрировать её сразу после успешного создания ресурса.
t.Fatal вызывает механизм FailNow: текущая функция теста немедленно прекращается, но testing продолжает завершение тестового объекта и запускает его cleanup-функции. Несколько cleanup-функций выполняются в обратном порядке регистрации.
В примере cfg.Close() будет вызван даже при ошибке валидации. Важно, что t.Fatal следует вызывать из горутины, в которой выполняется сам тест: вызов Fatal из фоновой горутины не завершает родительский тест корректно и может привести к некорректной синхронизации.
Для вложенных subtests очистка дочернего теста выполняется после его завершения, а очистка родительского — после завершения всех его subtests. Если cleanup-функций несколько, последняя зарегистрированная выполняется первой.
Тест создаёт временную конфигурацию, затем проверяет её загрузку. При ошибке загрузки используется t.Fatal, а удаление файла было записано обычной инструкцией в конце теста. При сбое файл оставался в рабочем каталоге CI, а последующие запуски могли читать устаревшее содержимое.
Рассматривались два варианта. defer в основном тесте прост, но хуже масштабируется, если создание ресурса вынесено в helper; ручная очистка в конце наиболее очевидна, но не выполняется после Fatal. Был выбран t.Cleanup сразу после создания ресурса: он сохраняет привязку очистки к тесту и срабатывает независимо от обычного пути выхода.
После изменения временные ресурсы удалялись и при успешном прохождении, и при раннем падении теста, а диагностика CI перестала зависеть от мусора предыдущего запуска.
1. Вопрос: В каком порядке выполняются несколько функций, зарегистрированных через t.Cleanup?
Они выполняются в обратном порядке регистрации. Это позволяет регистрировать очистку ресурсов в порядке их создания: последний созданный ресурс освобождается первым, как при корректном разматывании стека.
2. Вопрос: Выполнится ли cleanup родительского теста до завершения его параллельного subtest?
Нет. Cleanup родительского теста выполняется после завершения всех его дочерних subtests. Для subtest, вызвавшего t.Parallel, это означает, что родительская очистка не должна освобождать ресурс, пока дочерний тест ещё может его использовать.
3. Вопрос: Чем опасен вызов t.Fatal из горутины, созданной внутри теста?
t.Fatal завершает только текущую горутину через механизм, аналогичный runtime.Goexit, а не весь тестовый сценарий. Родительский тест может продолжить выполнение или завершиться раньше фоновой работы, поэтому для передачи ошибки нужно синхронизировать горутину и сообщить результат из основной тестовой горутины, например через канал или sync.WaitGroup.