Go: Автоматическое тестирование

Теория: Табличные тесты

Когда мы пишем тесты, часто приходится проверять одну и ту же функцию на разных входных данных. Например, у нас есть функция 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.

Рекомендуемые программы

+7 800 100 22 47

бесплатно по РФ

+7 495 085 21 62

бесплатно по Москве

108813 г. Москва, вн.тер.г. поселение Московский,
г. Московский, ул. Солнечная, д. 3А, стр. 1, помещ. 20Б/3
ОГРН 1217300010476
ИНН 7325174845