- Пример без таблицы
- Табличный подход
- Ещё один пример: проверка строк
- Подтесты (t.Run)
- Табличные тесты и ошибки
Когда мы пишем тесты, часто приходится проверять одну и ту же функцию на разных входных данных. Например, у нас есть функция Max
, которая должна возвращать большее из двух чисел. Проверять её только на одном кейсе бессмысленно — надо убедиться, что она работает правильно в разных ситуациях: когда первое число больше, когда второе больше, когда числа равны.
Можно писать отдельный if
для каждого случая, но это быстро превращается в дублирование. Чтобы сделать тесты компактнее, Go-разработчики используют приём табличных тестов.
Пример без таблицы
Сначала посмотрим, как будет выглядеть тест:
func TestMax_NoTable(t *testing.T) {
// проверка 1: второе число больше
if got := Max(2, 3); got != 3 {
t.Errorf("Max(2, 3) = %d, хотели 3", got)
}
// проверка 2: числа равны
if got := Max(5, 5); got != 5 {
t.Errorf("Max(5, 5) = %d, хотели 5", got)
}
// проверка 3: первое число больше
if got := Max(10, 3); got != 10 {
t.Errorf("Max(10, 3) = %d, хотели 10", got)
}
}
Вроде всё ок, но три раза повторяется один и тот же код: вызвали функцию, сравнили результат, вывели сообщение. Если кейсов станет 10 или 20 — тест будет раздутым.
Табличный подход
Чтобы не повторяться, сделаем «таблицу кейсов» — срез структур, где каждая структура хранит входные данные и ожидаемый результат.
func TestMax_Table(t *testing.T) {
// Таблица кейсов: три строки = три сценария
cases := []struct {
a, b int // входные данные
want int // ожидаемый результат
}{
{2, 3, 3}, // второй больше
{5, 5, 5}, // равные
{10, 3, 10}, // первый больше
}
// Циклом пробегаем по всем сценариям
for _, c := range cases {
got := Max(c.a, c.b)
if got != c.want {
// Если результат не совпал — ошибка
t.Errorf("Max(%d, %d) = %d, хотели %d", c.a, c.b, got, c.want)
}
}
}
Плюсы такого подхода:
- Код теста короткий и не повторяется.
- Легко добавить новые кейсы: просто ещё одна строка в таблице.
- Читается как список условий: удобно глазами пробегать, что проверяется.
Ещё один пример: проверка строк
Представим функцию, которая делает первую букву строки заглавной:
func Capitalize(s string) string {
if s == "" {
return ""
}
return strings.ToUpper(s[:1]) + s[1:]
}
Для неё тоже удобно написать табличный тест:
func TestCapitalize(t *testing.T) {
cases := []struct {
in string
want string
}{
{"hello", "Hello"},
{"go", "Go"},
{"", ""}, // пустая строка — отдельный сценарий
}
for _, c := range cases {
got := Capitalize(c.in)
if got != c.want {
t.Errorf("Capitalize(%q) = %q, хотели %q", c.in, got, c.want)
}
}
}
Здесь сразу видно, какие варианты проверяются: обычное слово, короткая строка, пустая строка.
Подтесты (t.Run)
Чтобы ещё удобнее видеть, какой именно кейс сломался, можно запускать каждый сценарий как отдельный подтест. Для этого используется метод t.Run
.
func TestMax_Subtests(t *testing.T) {
cases := []struct {
a, b int
want int
}{
{2, 3, 3},
{5, 5, 5},
{10, 3, 10},
}
for _, c := range cases {
// имя подтеста формируем из входных данных
name := fmt.Sprintf("%d_%d", c.a, c.b)
t.Run(name, func(t *testing.T) {
got := Max(c.a, c.b)
if got != c.want {
t.Errorf("Max(%d, %d) = %d, хотели %d", c.a, c.b, got, c.want)
}
})
}
}
Теперь вывод тестов будет выглядеть так:
=== RUN TestMax_Subtests
=== RUN TestMax_Subtests/2_3
=== RUN TestMax_Subtests/5_5
=== RUN TestMax_Subtests/10_3
--- FAIL: TestMax_Subtests (0.00s)
--- FAIL: TestMax_Subtests/2_3 (0.00s)
Очень удобно, когда кейсов много: сразу видно, какой именно вход сломался.
Табличные тесты и ошибки
Обычно вместе с входными данными и результатом в таблицу добавляют и ожидаемую ошибку.
Функция:
func Divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
Табличный тест:
func TestDivide(t *testing.T) {
cases := []struct {
a, b int
want int
err error
}{
{10, 2, 5, nil}, // обычное деление
{10, 0, 0, errors.New("division by zero")}, // ошибка деления на ноль
}
for _, c := range cases {
got, err := Divide(c.a, c.b)
if c.err != nil {
// если ожидалась ошибка — проверяем её наличие
if err == nil || err.Error() != c.err.Error() {
t.Errorf("Divide(%d, %d) ожидали ошибку %q, получили %v",
c.a, c.b, c.err, err)
}
continue
}
// если ошибки не ожидали — сравниваем результат
if got != c.want {
t.Errorf("Divide(%d, %d) = %d, хотели %d", c.a, c.b, got, c.want)
}
}
}
Таким образом можно в одной таблице описать и успешные сценарии, и сценарии с ошибкой.
Табличные тесты в Go — это мощный приём, который делает код чище и позволяет легко масштабировать проверки. Логика теста описывается всего один раз, а сами сценарии выносятся в таблицу, так что добавление новых случаев сводится к дописыванию строки. Использование t.Run
помогает сразу увидеть, какой именно вариант сломался, а значит отладка становится проще. Такой подход одинаково удобен как для обычных функций, так и для функций, которые возвращают ошибки. Именно поэтому табличные тесты считаются одним из самых популярных и практичных паттернов тестирования в Go.
Самостоятельная работа
Напишите функцию, которая нормализует строки по простым правилам:
- Удаляет ведущие и замыкающие пробелы.
- Последовательности пробелов и табов внутри заменяет на один пробел.
- Приводит ASCII-буквы к нижнему регистру.
package normalize
import "strings"
// Clean trims spaces, collapses internal spaces to one
// and lowercases ASCII letters.
func Clean(s string) string {
// Убираем крайние пробелы/табы, схлопываем внутренние пробелы/табы,
// приводим к нижнему регистру.
tokens := strings.Fields(s)
if len(tokens) == 0 {
return ""
}
return strings.ToLower(strings.Join(tokens, " "))
}
Сделайте табличный тест, который покрывает разные сценарии:
- Пустая строка.
- Одна и несколько внутренних групп пробелов/табов.
- Разный регистр вхождения (миксы верх/низ).
- Уже нормализованная строка.
- Строка, содержащая небуквенные символы и несколько слов.
Показать решение
package normalize
import "testing"
func TestClean(t *testing.T) {
tests := []struct {
name string
in string
want string
}{
{name: "empty", in: "", want: ""},
{name: "spaces", in: " hello world ", want: "hello world"},
{name: "tabs", in: "\thexlet\t go\t", want: "hexlet go"},
{name: "case", in: "HeXLet", want: "hexlet"},
{name: "mixed", in: " A b\tC ", want: "a b c"},
{name: "punct", in: " Hello, world! ", want: "hello, world!"},
}
for _, tc := range tests {
got := Clean(tc.in)
if got != tc.want {
t.Errorf("%s: got %q, want %q", tc.name, got, tc.want)
}
}
}
Дополнительные материалы
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.