Перейти к основному содержимому
Модуль 9 · Урок 2 из 542/50 в курсе~14 min
Уроки модуля (2/5)

Табличные тесты

Табличное тестирование — это идиоматический шаблон Go: вместо написания N отдельных функций TestThisSpecificCase вы объявляете часть случаев и выполняете итерацию. Существует ОДИН блок утверждений, и тестовые данные отделены от логики.

Базовый узор

Go
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)
            }
        })
    }
}

Три ингредиента:

  1. Фрагмент анонимной структуры с обычными полями: name, входные данные, ожидаемый результат (и, возможно, ожидаемая ошибка).
  2. for _, tc := range cases — цикл выполнения.
  3. 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() внутри функции для распараллеливания.
Bash
go test -v -run TestAbs/neg

Случаи с ожидаемыми ошибками

Go
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 приводил к тому, что все подтесты выполнялись для одного и того же последнего случая. Исправление было:

Go
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 (или используйте подчеркивание).
  • Всегда включайте случай «счастливого пути», случай «края» (ноль, пустой) и хотя бы один случай ошибки.

Упражнения

Упражнение#go.m9.l2.e1
Попыток: 0Загрузка…

Определите фрагмент случаев анонимной структуры с полями и хотите, заполненный тремя случаями для проверки Abs (положительный, отрицательный, нулевой).

Загрузка редактора…

Решение доступно после 3 попыток

Упражнение#go.m9.l2.e2
Попыток: 0Загрузка…

Добавьте t.Run с tc.name, чтобы превратить каждый случай в именованный подтест.

Загрузка редактора…

Решение доступно после 3 попыток

Quiz#go.m9.l2.e3
Готово

В чем основное преимущество табличного шаблона перед N отдельными функциями тестирования?

Go
for _, tc := range cases {
  t.Run(tc.name, func(t *testing.T) { ... })
}
Варианты ответа