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

Теория: Использование testing.TB и вспомогательных функций

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

Чтобы такие хелперы были универсальными и работали не только в обычных тестах (*testing.T), но и в бенчмарках (*testing.B), в стандартной библиотеке есть общий интерфейс testing.TB.

Что такое testing.TB

TB — это интерфейс, который описывает всё поведение теста: логирование, падение, пропуск, отметку ошибки.

Выглядит он так:

type TB interface {
	Error(args ...any)
	Errorf(format string, args ...any)
	Fail()
	FailNow()
	Fatal(args ...any)
	Fatalf(format string, args ...any)
	Helper()
	Log(args ...any)
	Logf(format string, args ...any)
	Name() string
	Skip(args ...any)
	SkipNow()
	Skipf(format string, args ...any)
	Skipped() bool
}

Главное: и *testing.T, и *testing.B реализуют этот интерфейс. Поэтому если функция принимает t testing.TB, её можно вызывать и в тестах, и в бенчмарках.

Пример: без хелпера и с ним

Функция Max:

// Max возвращает большее из двух чисел.
func Max(a, b int) int {
	if a > b {
		return a
	}
	return b
}

тест:

func TestMax_NoHelper(t *testing.T) {
	// кейс 1: второе число больше
	got := Max(2, 3)
	want := 3
	if got != want {
		t.Errorf("Max(2, 3) = %d, хотели %d", got, want)
	}

	// кейс 2: первое число больше
	got = Max(10, 5)
	want = 10
	if got != want {
		t.Errorf("Max(10, 5) = %d, хотели %d", got, want)
	}

	// кейс 3: равные числа
	got = Max(7, 7)
	want = 7
	if got != want {
		t.Errorf("Max(7, 7) = %d, хотели %d", got, want)
	}
}

Три раза повторяется одно и то же сравнение.

Вынесем проверку в хелпер:

// assertEqual проверяет равенство чисел.
// Если не равны — помечает тест упавшим.
func assertEqual(t testing.TB, got, want int) {
	t.Helper() // говорит Go: эта функция — хелпер
	if got != want {
		t.Errorf("получили %d, хотели %d", got, want)
	}
}

Теперь тест выглядит компактно:

func TestMax_WithHelper(t *testing.T) {
	assertEqual(t, Max(2, 3), 3)
	assertEqual(t, Max(10, 5), 10)
	assertEqual(t, Max(7, 7), 7)
}

Почему нужен t.Helper()

Если его убрать, Go покажет ошибку в строке внутри assertEqual, а не там, где он был вызван. С ним же ошибка «поднимается» в настоящий тест, и сразу видно, какой именно кейс упал.

Хелперы для ошибок

Функция Divide:

func Divide(a, b int) (int, error) {
	if b == 0 {
		return 0, errors.New("division by zero")
	}
	return a / b, nil
}

тест:

func TestDivide_NoHelper(t *testing.T) {
	_, err := Divide(10, 0)
	if err == nil {
		t.Fatal("ожидали ошибку, но получили nil")
	}
	if err.Error() != "division by zero" {
		t.Errorf("получили ошибку %q, хотели %q", err.Error(), "division by zero")
	}
}

Хелпер для ошибок:

// assertError проверяет, что ошибка есть и её сообщение совпадает с ожидаемым.
func assertError(t testing.TB, err error, want string) {
	t.Helper()
	if err == nil {
		t.Fatal("ожидали ошибку, но получили nil")
	}
	if err.Error() != want {
		t.Errorf("получили ошибку %q, хотели %q", err.Error(), want)
	}
}

Тест становится аккуратным:

func TestDivide(t *testing.T) {
	_, err := Divide(10, 0)
	assertError(t, err, "division by zero")
}

Хелперы для разных типов

Можно делать хелперы для любых проверок:

func assertString(t testing.TB, got, want string) {
	t.Helper()
	if got != want {
		t.Errorf("получили строку %q, хотели %q", got, want)
	}
}

func assertBool(t testing.TB, got, want bool) {
	t.Helper()
	if got != want {
		t.Errorf("получили %v, хотели %v", got, want)
	}
}

Что такое бенчмарки

Бенчмарк — это тест не на правильность, а на скорость. Обычный тест проверяет «функция работает верно или нет»,а бенчмарк отвечает на вопрос «насколько быстро она работает».

Пример:

func Add(a, b int) int {
	return a + b
}

func BenchmarkAdd(b *testing.B) {
	for i := 0; i < b.N; i++ {
		_ = Add(1, 2) // измеряем скорость вызова
	}
}

Запуск:

go test -bench=.

Вывод:

BenchmarkAdd-8   	1000000000	         0.300 ns/op

Здесь 0.300 ns/op — время одного вызова в наносекундах.

Хелперы и бенчмарки

Так как и *testing.T, и *testing.B реализуют testing.TB, хелперы можно использовать и там, и там.

func BenchmarkMax(b *testing.B) {
	for i := 0; i < b.N; i++ {
		got := Max(10, 5)
		assertEqual(b, got, 10) // тот же хелпер, что и в тесте
	}
}

Один и тот же код проверок работает в тестах и в бенчмарках.

Итог

  • testing.TB — общий интерфейс для тестов и бенчмарков.
  • Хелперы (assertEqual, assertError и т.п.) позволяют вынести повторяющийся код.
  • t.Helper() нужно, чтобы ошибки указывали на строку вызова, а не на сам хелпер.
  • Бенчмарки проверяют скорость работы кода, и в них тоже можно использовать хелперы.

Так тесты становятся чище, бенчмарки — удобнее, а проект — поддерживаемее.

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

+7 800 100 22 47

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

+7 495 085 21 62

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

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