← Назад к вопросам

Что такое table-driven tests?

1.6 Junior🔥 91 комментариев
#Тестирование

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI7 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Что такое Table-Driven Tests?

Table-Driven Tests (табличные тесты) — это методология написания модульных тестов, при которой тестовые данные и ожидаемые результаты организуются в виде таблицы (обычно срез структур или карт), а затем итерируются в едином тестовом цикле. Этот подход широко используется в Go-сообществе благодаря своей чистоте, компактности и удобству расширения.

Основная идея и структура

Вместо написания множества отдельных тестовых функций для различных входных данных создаётся одна тестовая функция, которая перебирает заранее определённые тестовые кейсы. Каждый кейс содержит:

  • Входные параметры (аргументы тестируемой функции)
  • Ожидаемый результат
  • Описание кейса (для понятных сообщений об ошибках)

Пример базовой структуры на Go:

func TestAdd(t *testing.T) {
    // Определение таблицы тестовых случаев
    testCases := []struct {
        name     string // Описание теста
        a, b     int    // Входные данные
        expected int    // Ожидаемый результат
    }{
        {"positive numbers", 2, 3, 5},
        {"negative numbers", -1, -1, -2},
        {"mixed signs", -5, 10, 5},
        {"zero identity", 0, 42, 42},
    }

    // Итерация по всем тестовым случаям
    for _, tc := range testCases {
        // Запуск подтеста с именем случая
        t.Run(tc.name, func(t *testing.T) {
            actual := Add(tc.a, tc.b)
            if actual != tc.expected {
                t.Errorf("Add(%d, %d): expected %d, got %d", tc.a, tc.b, tc.expected, actual)
            }
        })
    }
}

Ключевые преимущества

  • Компактность и чистота кода: Устраняется дублирование кода вызова тестируемой функции и проверок. Новая логика добавляется только в таблицу.
  • Лёгкость расширения: Чтобы добавить новый тестовый случай, достаточно дописать одну строку в таблицу, не создавая новых функций.
  • Удобство поддержки: Все тестовые сценарии для одной функции собраны в одном месте, что упрощает их обзор и модификацию.
  • Чёткие сообщения об ошибках: Использование поля name в каждом кейсе позволяет точно идентифицировать, какой именно сценарий не прошёл проверку.
  • Совместимость с t.Run(): Каждый кейс можно запускать как отдельный подтест (subtest), что даёт детализированный вывод (go test -v) и возможность выборочного запуска (go test -run TestAdd/positive).

Расширенные возможности и паттерны

1. Тестирование функций, возвращающих ошибки:

func TestParseInput(t *testing.T) {
    tests := []struct {
        name    string
        input   string
        want    *Result
        wantErr bool
    }{
        {"valid input", "42", &Result{Value: 42}, false},
        {"empty input", "", nil, true},
        {"invalid format", "abc", nil, true},
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got, err := ParseInput(tt.input)
            if (err != nil) != tt.wantErr {
                t.Errorf("error = %v, wantErr %v", err, tt.wantErr)
                return
            }
            if !reflect.DeepEqual(got, tt.want) {
                t.Errorf("got = %v, want %v", got, tt.want)
            }
        })
    }
}

2. Использование карт (maps) для динамических названий:

func TestToUpperCase(t *testing.T) {
    tests := map[string]struct {
        input string
        want  string
    }{
        "simple word":    {"hello", "HELLO"},
        "with spaces":    {"hello world", "HELLO WORLD"},
        "empty string":   {"", ""},
    }
    for name, tc := range tests {
        t.Run(name, func(t *testing.T) {
            if got := ToUpperCase(tc.input); got != tc.want {
                t.Errorf("got %q, want %q", got, tc.want)
            }
        })
    }
}

Рекомендации и лучшие практики

  • Всегда используйте поле name для описания тестового случая — это критически важно для отладки.
  • Для сложных структур сравнения применяйте reflect.DeepEqual или специализированные библиотеки вроде google/go-cmp.
  • Выделяйте большие таблицы тестов в отдельные переменные или даже файлы, если они используются в нескольких тестовых функциях.
  • Избегайте излишней абстракции — табличные тесты должны оставаться простыми и читаемыми. Если логика проверки становится сложной, возможно, стоит рассмотреть другой подход.
  • Помните о производительности при огромном количестве кейсов (десятки тысяч), хотя на практике это редко становится проблемой.

Заключение

Table-Driven Tests — это идиоматичный и мощный паттерн Go, который превращает написание тестов из рутинного процесса в эффективный и структурированный. Он не только сокращает объём кода, но и значительно повышает его поддерживаемость, делая тестовые сценарии прозрачными и легко анализируемыми. Именно поэтому этот подход де-факто является стандартом в экосистеме Go и настоятельно рекомендуется к использованию в production-проектах.