GoMiddleCoding
Что такое table-driven testing и почему это идиоматично для Go?
Table-driven tests — это срез структур с тест-кейсами, прокручиваемый в цикле с t.Run для именованных подтестов. Стандарт Go: уменьшает дублирование, упрощает добавление случаев и позволяет запускать конкретный подтест через -run.
Table-driven tests в Go
Table-driven testing — идиоматический подход к тестированию в Go: тест-случаи описываются как срез структур (таблица), а единый цикл прогоняет каждый из них. Это уменьшает дублирование кода и упрощает добавление новых случаев.
Базовый пример
package math_test
import (
"testing"
)
func Divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("division by zero")
}
return a / b, nil
}
func TestDivide(t *testing.T) {
tests := []struct {
name string
a, b float64
want float64
wantErr bool
}{
{name: "positive", a: 10, b: 2, want: 5},
{name: "negative divisor", a: 10, b: -2, want: -5},
{name: "divide by zero", a: 5, b: 0, wantErr: true},
{name: "zero numerator", a: 0, b: 3, want: 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Divide(tt.a, tt.b)
if (err != nil) != tt.wantErr {
t.Errorf("Divide() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr && got != tt.want {
t.Errorf("Divide() = %v, want %v", got, tt.want)
}
})
}
}
Почему это идиоматично для Go
- Стандартная библиотека Go использует этот паттерн повсеместно (см.
strings,strconv,net/http). t.Runсоздаёт именованный подтест, который можно запустить отдельно:go test -run TestDivide/divide_by_zero.- Каждый подтест изолирован: сбой одного не останавливает остальные.
- Структура данных явно документирует контракт функции.
Параллельное выполнение
for _, tt := range tests {
tt := tt // захват переменной цикла (обязательно до Go 1.22)
t.Run(tt.name, func(t *testing.T) {
t.Parallel() // подтесты выполняются параллельно
// ...
})
}
Запуск конкретного случая
go test -run TestDivide/positive ./...
go test -run TestDivide -v ./... # показать все подтесты
Расширенный паттерн: setup и teardown
tests := []struct {
name string
setup func() *DB
input string
want int
}{
{
name: "empty db",
setup: func() *DB { return NewTestDB(t) },
input: "query",
want: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
db := tt.setup()
defer db.Close()
// ...
})
}
Подводные камни
- До Go 1.22 захват переменной цикла (
tt := tt) обязателен при использованииt.Parallel(); без него все горутины разделяют одинttи тестируют только последний случай. - Использование
t.Fatalвнутри горутины не останавливает тест — вызывает панику. В подтестах сt.Parallel()используйтеt.Error. - Слишком общая таблица с десятками полей-флагов вместо отдельных тестовых функций — антипаттерн; дробите по смысловым группам.
- Имена случаев с пробелами:
t.Runзаменяет пробелы на_в выводе, поэтому фильтр-runнужно писать с_или использоватьregexp. - Тест не падает при добавлении нового случая, если забыли добавить поле в структуру: Go инициализирует пропущенные поля нулевыми значениями без ошибки компиляции.
- Не смешивайте таблицу с глобальным состоянием: если тест-случаи зависят от порядка выполнения, таблица теряет смысл.
Common mistakes
- Давать ответ про table-driven testing только на уровне определения, не показывая поведение в реальном приложении.
- Игнорировать границы ответственности вокруг темы «table-driven testing»: кто отменяет работу, кто владеет ресурсом и где формируется ответ клиенту.
- Не связывать table-driven testing с observability, тестированием или безопасностью, когда это влияет на продакшен-поведение.
What the interviewer is testing
- Точно объясняет, что именно делает table-driven testing и где это используется в Go-коде.
- Связывает table-driven testing с корректным lifecycle запроса, отменой, конкурентностью или конфигурацией сервера там, где это уместно.
- Не изобретает API и опирается на реальные контракты официальной документации.