Уроки модуля (2/5)
Табличные тесты
Табличное тестирование — это идиоматический шаблон Go: вместо написания N отдельных функций TestThisSpecificCase вы объявляете часть случаев и выполняете итерацию. Существует ОДИН блок утверждений, и тестовые данные отделены от логики.
Базовый узор
func TestAbs(t *testing.T) {
cases := []struct {
name string
in int
want int
}{
{"pos", 3, 3},
{"neg", -3, 3},
{"zero", 0, 0},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := Abs(tc.in); got != tc.want {
t.Errorf("Abs(%d) = %d; voglio %d", tc.in, got, tc.want)
}
})
}
}Три ингредиента:
- Фрагмент анонимной структуры с обычными полями:
name, входные данные, ожидаемый результат (и, возможно, ожидаемая ошибка). for _, tc := range cases— цикл выполнения.t.Run(tc.name, func(t *testing.T) { ... })— каждый кейс становится подтестом со своим названием.
Почему t.Run?
t.Run создает подтест, который имеет:
- Индивидуальное имя отображается в выводе:
TestAbs/pos,TestAbs/neg, ... - Свой
t *testing.T: сбой в одном случае не останавливает другие (если не используетсяt.Fatal). - Фильтрация командной строки:
go test -run TestAbs/negработает только в этом случае. - Необязательный параллелизм: вызовите
t.Parallel()внутри функции для распараллеливания.
go test -v -run TestAbs/negСлучаи с ожидаемыми ошибками
cases := []struct {
name string
in string
want int
wantErr bool
}{
{"ok", "42", 42, false},
{"bad", "x", 0, true},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got, err := Atoi(tc.in)
if (err != nil) != tc.wantErr {
t.Fatalf("err = %v, wantErr = %v", err, tc.wantErr)
}
if !tc.wantErr && got != tc.want {
t.Errorf("got %d, want %d", got, tc.want)
}
})
}Ошибка замыкания цикла (до Go 1.22)
В версиях до Go 1.22 переменная tc была общей на всех итерациях: вызов t.Parallel() внутри t.Run приводил к тому, что все подтесты выполнялись для одного и того же последнего случая. Исправление было:
for _, tc := range cases {
tc := tc // local rebind (pre-1.22)
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// ...
})
}Именование дел
Лучшие практики:
- Короткое, но запоминающееся имя:
"empty input","too large","negative". - Никаких пробелов, если вы хотите легко фильтровать с помощью
-run(или используйте подчеркивание). - Всегда включайте случай «счастливого пути», случай «края» (ноль, пустой) и хотя бы один случай ошибки.
Упражнения
Определите фрагмент случаев анонимной структуры с полями и хотите, заполненный тремя случаями для проверки Abs (положительный, отрицательный, нулевой).
Решение доступно после 3 попыток
Добавьте t.Run с tc.name, чтобы превратить каждый случай в именованный подтест.
Решение доступно после 3 попыток
В чем основное преимущество табличного шаблона перед N отдельными функциями тестирования?
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) { ... })
}