Перейти к содержимому
Теория · От файла к проекту · урок 7 из 10

Табличные тесты и t.Run

В прошлом уроке тест проверял один вызов Clamp. Но у функции обычно много интересных случаев: отрицательное число, ноль, граница, очень большое значение. Если на каждый случай писать свой if с t.Errorf, тест быстро разрастается, а случаи теряются среди одинаковых строк.

На заводе с этим справляются так: контролёр заводит на каждую деталь свой лист проверки. На листе написано, какая это деталь, что на входе и что должно получиться. В Go такой стопкой листов служит таблица случаев, а отдельный лист для каждой детали даёт t.Run.

Проверять будем функцию Abs из пакета abs, которая возвращает модуль числа:

GO
package abs

func Abs(x int) int {
    if x < 0 {
        return -x
    }
    return x
}

Таблица случаев

Табличный тест — это срез случаев и один цикл, который проверяет их по очереди. Вот тест для Abs в файле abs_test.go, с теми же строками package abs и import "testing" наверху:

GO
func TestAbs(t *testing.T) {
    cases := []struct {
        name    string
        x, want int
    }{
        {"negative", -7, 7}, {"zero", 0, 0}, {"positive", 4, 4},
    }
    for _, c := range cases {
        t.Run(c.name, func(t *testing.T) {
            if got := Abs(c.x); got != c.want {
                t.Errorf("Abs(%d) = %d, want %d", c.x, got, c.want)
            }
        })
    }
}

Разберём его по частям:

  1. []struct { … } — срез структур без имени типа. Каждая структура — одна строка таблицы.
  2. Поля строки: name — имя случая, x — вход, want — ожидаемый результат.
  3. {"negative", -7, 7} — одна строка: случай «negative», вход -7, ждём 7.
  4. Цикл for проходит по всем строкам и для каждой вызывает Abs(c.x).

Новый случай — это одна новая строка таблицы, а код проверки остаётся прежним. Так тест растёт по строкам, а не по копиям if.

t.Run и подтесты

t.Run(c.name, func(t *testing.T) { … }) запускает функцию как отдельный подтест с именем случая. Вот что это даёт:

  • у подтеста своё имя в отчёте: TestAbs/negative;
  • подтест проходит или проваливается сам по себе, и провал одного случая не прячет остальные;
  • внутри функции свой параметр t, и t.Fatalf через него останавливает только этот подтест — вы видели это в виджете прошлого урока.

Вот что печатает go test -v:

TEXT
$ go test -v
=== RUN   TestAbs
=== RUN   TestAbs/negative
=== RUN   TestAbs/zero
=== RUN   TestAbs/positive
--- PASS: TestAbs (0.00s)
    --- PASS: TestAbs/negative (0.00s)
    --- PASS: TestAbs/zero (0.00s)
    --- PASS: TestAbs/positive (0.00s)
PASS
ok  	example.com/abs	0.003s

Каждый случай получил свою строку === RUN и свой итог --- PASS. Если бы Abs ошибалась на отрицательных числах, в отчёте была бы строка --- FAIL: TestAbs/negative, а zero и positive всё равно прошли бы проверку.

Один подтест отдельно

Флаг -run запускает только те тесты, чьё имя подходит под шаблон. Имя подтеста пишут через косую черту:

TEXT
$ go test -v -run TestAbs/negative
=== RUN   TestAbs
=== RUN   TestAbs/negative
--- PASS: TestAbs (0.00s)
    --- PASS: TestAbs/negative (0.00s)
PASS
ok  	example.com/abs	0.004s

Это удобно, когда чините один случай: не нужно ждать остальные и искать нужную строку в длинном отчёте. На собеседовании про табличные тесты и t.Run спрашивают часто: так написана большая часть тестов на Go.

Здесь картинка с листами проверки перестаёт работать. На заводе лист с именем детали заводит контролёр, и двух одинаковых листов он не допустит. В Go имя подтеста — это строка name из таблицы, и Go не требует, чтобы имена были разными: повторное имя он молча переименует.

Частая ошибка

Строку таблицы скопировали, а имя поменять забыли:

GO
{"negative", -7, 7}, {"zero", 0, 0}, {"zero", 4, 4}, // ✗
TEXT
=== RUN   TestAbs
=== RUN   TestAbs/negative
=== RUN   TestAbs/zero
=== RUN   TestAbs/zero#01
--- PASS: TestAbs (0.00s)
    --- PASS: TestAbs/negative (0.00s)
    --- PASS: TestAbs/zero (0.00s)
    --- PASS: TestAbs/zero#01 (0.00s)
PASS
ok  	example.com/abs	0.004s

Тест проходит, но go test молча переименовал второй случай в zero#01. Если такой случай провалится, по отчёту не понять, какая это строка, а -run TestAbs/zero запустит оба. Давайте каждому случаю своё имя, например positive.

Главное

  • Табличный тест — срез структур с полями name, входом и ожидаемым результатом плюс один цикл.
  • t.Run(c.name, func(t *testing.T) { … }) делает каждый случай отдельным подтестом TestAbs/negative.
  • Подтест проваливается сам по себе, а t.Fatalf внутри него останавливает только его.
  • go test -run TestAbs/negative запускает один подтест; имена случаев должны быть разными.

Подпишись, чтобы пройти курс

Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.

Войти другим способом