Go · Testing — Table-driven tests: паттерн из стандартной библиотеки
В Go нет describe, it, expect(...).toBe(...) — только t.Errorf. Но вместо внешних фреймворков Go предлагает идиому table-driven tests: один срез структур — все входы, выходы и имена кейсов. Добавить новый случай — одна строка. Увидеть что упало — конкретное имя в выводе.
Зачем это нужно
В JS тесты запускаются через Jest, Mocha или Vitest — внешние инструменты, npm install перед первым тестом. В Go go test встроен в язык: никакой конфигурации, никакого runner’а.
Тест — функция в файле *_test.go с сигнатурой func TestXxx(t *testing.T). go test запускает её автоматически. Писать отдельную функцию на каждый входной случай многословно — table-driven tests решают это: все кейсы в одном месте, логика проверки — один раз.
Как это устроено
func TestReverse(t *testing.T) {
tests := []struct {
name string
input string
want string
}{
{"простая строка", "hello", "olleh"},
{"палиндром", "racecar", "racecar"},
{"пустая строка", "", ""},
{"один символ", "a", "a"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Reverse(tt.input)
if got != tt.want {
t.Errorf("Reverse(%q) = %q, want %q", tt.input, got, tt.want)
}
})
}
}
tt — соглашение для переменной в range tests. t.Run(name, func) создаёт подтест с именем. В выводе go test -v видно какой конкретно случай упал:
--- FAIL: TestReverse/простая_строка (0.00s)
strutil_test.go:28: Reverse("hello") = "hello", want "olleh"
Без t.Run при падении видно только TestReverse — непонятно на каком кейсе. С t.Run — точное имя.
Как использовать
Table-driven тест для функции, возвращающей ошибку:
func TestParsePort(t *testing.T) {
tests := []struct {
name string
input string
want int
wantErr bool
}{
{"валидный порт", "80", 80, false},
{"максимальный", "65535", 65535, false},
{"ноль", "0", 0, true},
{"отрицательный", "-1", 0, true},
{"не число", "abc", 0, true},
{"выше максимума", "99999", 0, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParsePort(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("ParsePort(%q): ошибка = %v, wantErr = %v", tt.input, err, tt.wantErr)
return
}
if !tt.wantErr && got != tt.want {
t.Errorf("ParsePort(%q) = %d, want %d", tt.input, got, tt.want)
}
})
}
}
wantErr bool — стандартный способ тестировать функции с ошибками. Проверяй факт ошибки, а не текст (err.Error()) — сообщения меняются, факт их наличия — нет.
Подводные камни
package foo vs package foo_test:
// ❌ package foo — белый ящик, видит неэкспортированное
package strutil
// ✅ package foo_test — чёрный ящик, тестирует публичный API
package strutil_test
foo_test — идиоматичный выбор: тестируешь то, что видит пользователь пакета.
Забыть t.Run — при падении не видно какой кейс:
// ❌ при падении сообщение без имени кейса
for _, tt := range tests {
got := Reverse(tt.input)
if got != tt.want {
t.Errorf(...)
}
}
// ✅ каждый кейс — именованный подтест
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { ... })
}
t.Errorf при сбое setup — тест продолжится в невалидном состоянии:
// ❌ Errorf не останавливает тест — f == nil, следующая строка запаникует
f, err := os.Create(tmpFile)
if err != nil {
t.Errorf("не смог создать файл: %v", err)
}
f.Write(data) // паника: f == nil
// ✅ Fatalf останавливает тест при сбое setup
f, err := os.Create(tmpFile)
if err != nil {
t.Fatalf("не смог создать файл: %v", err)
}
t.Fatalf — для setup (создание файлов, парсинг конфига). t.Errorf — для обычных assert: увидишь все провалы сразу, не только первый.
Лучшие практики
ttвrange tests— соглашение Go. Не изобретайtc,c,testCase.%qдля строк в Errorf — форматирует в кавычках, видна разница между"hello"и"hell o".- Имена кейсов — описание поведения, не входа —
"порт выше максимума"лучше чем"99999". t.Parallel()для независимых подтестов — ускоряет прогон при большом числе кейсов.
Итого
go testвстроен — никакого Jest/Mocha, тестирование из коробки с первой строки.t.Run— обязательно — без него при падении не видно какой кейс упал.package foo_test— чёрный ящик, тестирует публичный API. Предпочтительный выбор для юнит-тестов.wantErr bool— стандартный паттерн для функций с ошибками. Проверяй факт, не текст.- Дальше:
testifyдля assert без бойлерплейта,httptestдля тестирования HTTP-хендлеров, фаззинг черезgo test -fuzz.
Документация
- pkg.go.dev/testing — пакет testing: T, Run, Errorf, Fatalf, Parallel
- go.dev/blog/subtests — Go Blog: подтесты и sub-benchmarks
- go.dev/doc/code#Testing — How to Write Go Code: тестирование пакетов