Табличные тесты и t.Run
В прошлом уроке тест проверял один вызов Clamp. Но у функции обычно много интересных случаев: отрицательное число, ноль, граница, очень большое значение. Если на каждый случай писать свой if с t.Errorf, тест быстро разрастается, а случаи теряются среди одинаковых строк.
На заводе с этим справляются так: контролёр заводит на каждую деталь свой лист проверки. На листе написано, какая это деталь, что на входе и что должно получиться. В Go такой стопкой листов служит таблица случаев, а отдельный лист для каждой детали даёт t.Run.
Проверять будем функцию Abs из пакета abs, которая возвращает модуль числа:
package abs
func Abs(x int) int {
if x < 0 {
return -x
}
return x
}Таблица случаев
Табличный тест — это срез случаев и один цикл, который проверяет их по очереди. Вот тест для Abs в файле abs_test.go, с теми же строками package abs и import "testing" наверху:
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)
}
})
}
}Разберём его по частям:
[]struct { … }— срез структур без имени типа. Каждая структура — одна строка таблицы.- Поля строки:
name— имя случая,x— вход,want— ожидаемый результат. {"negative", -7, 7}— одна строка: случай «negative», вход-7, ждём7.- Цикл
forпроходит по всем строкам и для каждой вызываетAbs(c.x).
Новый случай — это одна новая строка таблицы, а код проверки остаётся прежним. Так тест растёт по строкам, а не по копиям if.
t.Run и подтесты
t.Run(c.name, func(t *testing.T) { … }) запускает функцию как отдельный подтест с именем случая. Вот что это даёт:
- у подтеста своё имя в отчёте:
TestAbs/negative; - подтест проходит или проваливается сам по себе, и провал одного случая не прячет остальные;
- внутри функции свой параметр
t, иt.Fatalfчерез него останавливает только этот подтест — вы видели это в виджете прошлого урока.
Вот что печатает go test -v:
$ 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 запускает только те тесты, чьё имя подходит под шаблон. Имя подтеста пишут через косую черту:
$ 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 не требует, чтобы имена были разными: повторное имя он молча переименует.
Частая ошибка
Строку таблицы скопировали, а имя поменять забыли:
{"negative", -7, 7}, {"zero", 0, 0}, {"zero", 4, 4}, // ✗=== 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 и подпишись на канал — откроются все уроки.