Table-test или как помочь агенту понять как тестироваться
- воскресенье, 14 июня 2026 г. в 00:00:05

Агенты сейчас пишут тесты уже за многими из нас, будем объективны, все больше людей кидаем таску ему - получают пачку аппрувов и погнали все это мержить
Но есть, скажем так, ненулевой шанс, что сгенерированные тесты окажутся произведением искусства мусором, который ничего не проверяет (и нет, не шутка, мне коллеги буквально показывали тесты где просто вызывается функция и проверяется что ошибка nil и на этом все, а результат просто всегда игнорировался)
И в таких кейсах очень классная картина: у вас зеленный CI, прод сломан, и вопрос:

И вот в чем суть, программиста от вайбкодера отличает в первую очередь понимание, а что делает агент и как подправить его проблемы? Поэтому понимать, как тесты устроены, всё ещё нужно - даже если руками вы их больше не набираете :)
Текст в первую очередь для тех, кто переходит на Go с других языков. Ну и для начинающих гоферов тоже.
Меня зовут Эдгар Сипки, я founder easyp & sipki tech и отбираю доклады на Golang Conf в программном комитете. А в своём тг-канале делюсь прикладными Go и LLM - инструментами и подходами для разработки - подписывайтесь, дальше будет больше :)
Самый популярный паттерн написания тестов в Go - это табличные тесты. Идея на самом деле простая, вместо десяти почти одинаковых тестов вы пишете один, а все вариации складываете в таблицу. Новый кейс - одна строка, то есть что входящие, что исходящие значения тестируемой логики прописываются параметрами тест кейса
Вторая ценность - скорость, что очень важно особо на больших проектах, ведь каждый тест кейс вы можете запустить конкурентно, где каждый тест становится subtest-ом через t.Parallel() , удобно перезапустить изолировано, самое важное не забывать про вызовы t.Helper() иначе дебагинг тестов станет отдельном котлом в аду
func TestParseAmount(t *testing.T) { t.Parallel() type testCase struct { in string want int } cases := []testCase{ {in: "100", want: 100}, {in: "1_000", want: 1000}, } for _, tc := range cases { t.Run("", func(t *testing.T) { t.Parallel() require.Equal(t, tc.want, ParseAmount(tc.in)) }) } }
К примеру, тут тип testCase живёт внутри теста и никому не мешает, но можно вынести структуру на уровень пакета. Звучит как чистая архитектура? Да и логично, зачем объявлять типы в тесте
Можно вынести структуру на уровень пакета. Звучит как чистая архитектура? Не тут-то было.
// Тип занят первым тестом... type testCase struct { in string want int } // ...а второму нужна своя структура. И понеслась. type testCase2 struct { in int want string } func TestParseAmount(t *testing.T) { /* использует testCase */ } func TestFormatAmount(t *testing.T) { /* использует testCase2 */ }
Тип нужен ровно одному тесту, а живёт на уровне пакета, а второй тест хочет свою структуру - и начинается: testCase2, parseTestCase, formatTestCase... в общем, зачем оно нам всем
Но, третий вариант (мой любимый) в целом не объявлять типы - ведь Go позволяет объявить слайс с анонимной структурой прямо в тест кейсе
cases := []struct { in string want int }{ {in: "100", want: 100}, {in: "1_000", want: 1000}, }
И тут минимум лишнего, но в любом из этих вариантов теста есть одна не самая удобная деталь для дебагинга - при поломке теста будет просто показан индекс поломанного кейс[а/ов]
--- FAIL: TestParseAmount/1
И тут на самом деле есть две развилки, первый добавить поле name в структуру тест кейса, но я обычно такой вариант не люблю так как его легко не проинициализировать, да и для меня тут логичнее выглядит классическая map где имя - и есть ключ
cases := map[string]struct { in string want int }{ "simple_number": {in: "100", want: 100}, "with_underscore": {in: "1_000", want: 1000}, } for name, tc := range cases { t.Run(name, func(t *testing.T) { t.Parallel() require.Equal(t, tc.want, ParseAmount(tc.in)) }) }
И тут важная деталь: порядок прохода по мапе в Go не гарантирован, а это значит что? Тесты не будут запускаться в порядка объявления
Кейсы будут выполняться каждый раз в разном порядке, и вот тут важно понять, это далеко не недостаток :)
Если вы пишите тесты, который зависят от порядка, то это уже очень хороший вопрос на ревью - тесты должны быть независимыми, и да, даже сабтесты аналогично должны быть независимыми, иначе это выстрелит в самый неподходищий момент
А map кейсы создают защиту от такого by design, то есть в целом вы как раз и сразу будете строить тесты исходя из логики, а не порядка и если кейсы начали падать "иногда" - значит, они связаны между собой, и лучше узнать об этом сейчас, а не в проде :)
И самое важное - это все позволяет “ограничивать” нашего агента, суть в чем: чем больше ограничений для агента, тем лучше нам, так как это снижает “разнообразие” вариантов у агента, что и увеличивает качество тех самых результатов, но, об этом уже в будущем в следующей статье :)
Структуру кейса объявляем внутри теста, а лучше анонимно, меньше кода, больше простоты. Каждому кейсу даём имя - это кратно упрощает дебагинг, а t.Run с t.Parallel() - используем всегда, это упрощает дебагинг + ускоряет запуск тестов за счет конкурентности
А слайс или мапа - дело вкуса :)
P.S. В телеграм-канале еще выложу ссылки на популярные скиллы по работе с тестами в Go, а также мою личную рекомендацию, как сделать свои собственные.