Для table driven unit теста подготовка входа обязательна для дальнейших проверок кейса. Какой способ сообще...

Для table-driven unit-теста подготовка входа обязательна для дальнейших проверок кейса. Какой способ сообщения об ошибке должен остановить только текущий subtest?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Используйте t.Fatal или t.Fatalf: они помечают текущий тест как проваленный и немедленно прекращают выполнение его тестовой горутины. Если остальные проверки всё ещё имеют смысл, применяйте t.Error или t.Errorf — они фиксируют ошибку, но позволяют продолжить текущий subtest.

Исторический контекст

В пакете testing разделены два сценария обработки ошибок: накопление независимых ошибок и немедленное прекращение невозможного сценария. Это позволяет тесту одновременно показывать несколько проблем либо не выполнять проверки, результаты которых уже нельзя интерпретировать.

Такое разделение особенно важно для table-driven тестов: каждый кейс запускается как отдельный subtest, поэтому критическая ошибка должна прекращать именно этот кейс, а не весь набор тестов.

Постановка проблемы

Предположим, тест сначала декодирует входные данные, а затем проверяет несколько свойств результата. Если декодирование не удалось, последующие проверки могут вызвать обращение к некорректному или пустому результату и породить вторичные, misleading-ошибки.

Если вместо Fatal использовать Error, тест продолжит выполнение. Это иногда полезно, но при невалидной подготовке скрывает первопричину и усложняет диагностику. Если безусловно использовать Fatal, можно потерять независимые сообщения об ошибках, которые помогли бы оценить все свойства уже корректного результата.

Подробное решение

t.Errorf помечает тест как проваленный, но управление возвращается следующей инструкции. Его выбирают для независимых проверок, когда ошибка одной проверки не делает остальные бессмысленными.

t.Fatalf сначала фиксирует провал, затем вызывает семантику FailNow: выполнение текущей тестовой горутины прекращается. Это не означает остановку всего процесса или родительского теста.

Минимальный пример:

func TestCases(t *testing.T) { t.Run("invalid setup", func(t *testing.T) { value, err := prepare() if err != nil { t.Fatalf("prepare: %v", err) } if value.Name == "" { t.Error("empty name") } }) }

После Fatalf тело данного subtest больше не выполняется. Родительский тест продолжит работу после возврата из t.Run, а результат t.Run будет false, если subtest завершился с ошибкой.

Критическое ограничение: Fatal, FailNow и их варианты нельзя вызывать из произвольной фоновой горутины. Они предназначены для горутины, выполняющей функцию теста или subtest; для фоновых работ нужно передать ошибку в тестовую горутину через канал, ожидание или другой механизм синхронизации.

Отложенные функции текущей горутины выполняются при завершении через FailNow, поэтому локальные defer не пропускаются. Зарегистрированные через t.Cleanup обработчики также выполняются тестовым фреймворком при завершении соответствующего теста.

Ситуация из практики

В тесте обработчика сначала создавался временный конфигурационный объект. При ошибке создания применялся t.Error, после чего тест обращался к частично инициализированной структуре. В CI это давало разные дополнительные ошибки в зависимости от входных данных.

Рассматривались два варианта. Безусловно использовать Fatal было бы безопаснее, но это скрывало бы независимые ошибки в проверках корректно созданной конфигурации. Оставить Error означало бы сохранить шум и риск паники.

Выбрали Fatalf только для ошибки подготовки, а для последующих независимых свойств — Errorf. В результате каждый некорректный кейс сообщал одну первопричину, а корректные кейсы по-прежнему показывали все нарушения своих ожиданий.

Что кандидаты часто упускают

  1. Останавливает ли t.Fatal родительский тест?

Нет. Он прекращает текущую тестовую горутину — в данном случае горутину subtest. Родитель продолжает выполнение после t.Run; при необходимости он может проверить возвращённое значение и прекратить свой сценарий отдельно.

  1. Выполняются ли defer после Fatal?

Да. Механизм завершения текущей тестовой горутины выполняет её отложенные функции. Однако полагаться на defer как на способ сообщить ошибку из фоновой горутины нельзя: проблема вызова Fatal из этой горутины остаётся.

  1. Почему нельзя вызвать Fatal из фоновой горутины, если тест всё равно ожидает её завершения?

Потому что FailNow завершает только горутину, в которой был вызван. Тестовая горутина может продолжить ожидание или завершиться независимо от этого вызова, а фреймворк не получит корректного управления завершением теста. Безопасный вариант — передать ошибку из рабочей горутины в тестовую и вызвать Fatalf уже там.