Зачем оборачивать каждый кейс table-driven теста Go в отдельный subtest, если важно получить независимые результаты всех кейсов?
Отдельный subtest через t.Run изолирует результат каждого кейса в отчёте: видно его имя, ошибку и длительность. Ошибка одного кейса помечает именно этот subtest, но не мешает последующим кейсам выполниться; весь родительский тест при этом завершится неуспешно.
Table-driven тесты появились как способ проверять одну операцию на множестве входов без копирования одинаковой структуры теста. Однако обычный цикл оставляет все проверки внутри одного тестового контекста: отчёт хуже связывает ошибку с конкретными данными, а остановка выполнения может скрыть остальные дефекты.
Механизм subtests в пакете testing решает эту проблему структурированием запуска. Каждый кейс получает собственное имя и состояние результата, сохраняя общую таблицу входных данных.
При проверке кейсов обычным циклом ошибка может быть выведена только с индексом или вручную добавленным описанием. Если проверка завершается через Fatal, последующие кейсы не выполнятся, поэтому один дефект способен скрыть несколько независимых ошибок.
Без subtests сложнее адресно перезапустить один сценарий, сопоставить падение с конкретным входом и анализировать отчёт в CI. При этом t.Run не делает данные или тестируемый объект автоматически независимыми: общие изменяемые ресурсы по-прежнему могут создавать взаимное влияние.
t.Run запускает функцию кейса в отдельном тестовом контексте. Имя subtest входит в путь теста, а вызовы t.Error или t.Fatal меняют результат текущего кейса, не превращая весь цикл в немедленно остановленный процесс.
В этом примере каждый кейс отображается отдельно, а t.Errorf позволяет продолжить проверки внутри конкретного subtest. Если заменить его на t.Fatalf, остановится только текущий subtest; следующие итерации цикла всё равно смогут выполниться.
Имя кейса должно быть содержательным и по возможности уникальным. Для диагностики полезно включать в таблицу отдельное поле name, а не строить описание ошибки только из индекса.
Subtests также можно запускать параллельно через t.Parallel, но это уже меняет требования к совместному состоянию и времени жизни ресурсов. Сначала нужно обеспечить отсутствие гонок и корректную синхронизацию, а затем оценивать пользу параллелизма.
В тесте парсера было несколько сотен кейсов. Вариант с одним циклом использовал меньше кода, но при Fatal останавливался на первом повреждённом входе, а сообщения содержали только числовой индекс. Вариант с t.Run дал более подробный отчёт и позволил быстро найти конкретный формат документа.
Рассматривались три подхода: оставлять обычный цикл, вручную добавлять индекс в каждую ошибку или создавать subtest для каждого кейса. Первый вариант плохо диагностировался, второй сохранял один общий контекст и всё равно мог преждевременно остановить цикл, а третий немного увеличивал структуру теста, зато обеспечивал адресуемые результаты.
Выбран был t.Run с понятными именами кейсов и последовательным выполнением: тест использовал общий кэш парсера, который не был рассчитан на конкурентный доступ. В результате CI показывал все ошибочные сценарии за один запуск, а исправление конкретного кейса стало проще проверять.
t.Run входные данные каждого кейса независимыми?Нет. t.Run изолирует прежде всего результат и контекст отчёта, но не копирует указатели, срезы, карты, глобальные переменные или внешние зависимости. Если один кейс изменяет общий объект, следующий может получить уже изменённое состояние; для независимости нужны новые данные, явный сброс или отдельный объект на каждый кейс.
t.Run?Обычно нет. Неуспешный subtest автоматически делает родительский тест неуспешным, поэтому дополнительная проверка результата нужна только если сам родитель должен принять особое решение или изменить дальнейшее поведение. Игнорирование результата не превращает провал subtest в успех.
Параллельный subtest приостанавливается на t.Parallel и продолжает выполнение после того, как его родительский тест дойдёт до завершения своей последовательной части. Все такие кейсы могут одновременно обращаться к общим данным, поэтому появятся требования к синхронизации, а порядок завершения станет недетерминированным.
Параллельность оправдана, когда кейсы действительно независимы и измеримо сокращают время тестов. Иначе она добавляет риск гонок, конфликтов за ресурсы и нестабильных тестов без полезного выигрыша.