Перейти до основного вмісту
Модуль 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Завантаження…

Визначте зріз випадків анонімної структури з полями in і want, заповненими 3 випадками для перевірки 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) { ... })
}
Варіанти відповідей