- Что такое 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()
нужно, чтобы ошибки указывали на строку вызова, а не на сам хелпер.- Бенчмарки проверяют скорость работы кода, и в них тоже можно использовать хелперы.
Так тесты становятся чище, бенчмарки — удобнее, а проект — поддерживаемее.
Самостоятельная работа
Протестируйте функцию Hello(name string) (string, error)
, которая возвращает строку-приветствие или ошибку, если имя пустое:
package greet
import "errors"
func Hello(name string) (string, error) {
if name == "" {
return "", errors.New("name cannot be empty")
}
return "Hello, " + name, nil
}
Для тестов создайте два хелпера:
assertString(t testing.TB, got, want string)
assertError(t testing.TB, got error, want bool)
Покройте следующие кейсы:
- Базовые случаи:
"Go"
,"World"
. - Пустая строка: ожидаем ошибку (
want = true
). - Юникод: например,
"Гофер"
возвращает"Hello, Гофер"
. - Сохранение пробелов: вход
" Go "
возвращает"Hello, Go "
(два пробела подряд допустимы, т.к. функция не триммит).
Показать решение
package greet
import "testing"
// assertString — минимальный хелпер сравнения строк.
func assertString(t testing.TB, got, want string) {
t.Helper()
if got != want {
t.Fatalf("mismatch: got %q, want %q", got, want)
}
}
// assertError — проверяет, возникла ли ошибка в соответствии с ожиданием.
func assertError(t testing.TB, got error, want bool) {
t.Helper()
if (got != nil) != want {
if want {
t.Fatalf("ожидалась ошибка, но её нет")
}
t.Fatalf("не ожидалась ошибка, но получена: %v", got)
}
}
func TestHello(t *testing.T) {
t.Run("basic", func(t *testing.T) {
got, err := Hello("Go")
assertError(t, err, false)
assertString(t, got, "Hello, Go")
got, err = Hello("World")
assertError(t, err, false)
assertString(t, got, "Hello, World")
})
t.Run("empty", func(t *testing.T) {
got, err := Hello("")
assertError(t, err, true)
assertString(t, got, "")
})
t.Run("unicode", func(t *testing.T) {
got, err := Hello("Гофер")
assertError(t, err, false)
assertString(t, got, "Hello, Гофер")
})
t.Run("spaces are preserved", func(t *testing.T) {
got, err := Hello(" Go ")
assertError(t, err, false)
assertString(t, got, "Hello, Go ")
})
}
Дополнительные материалы
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.