В стандартном пакете testing
проверки строятся вокруг выражений вида if got != want { t.Errorf(...) }
. Это надёжный способ, но при большом количестве тестов он делает код громоздким и плохо читаемым. Чтобы сделать тесты короче и понятнее, в Go часто используют библиотеку testify. Она предоставляет два основных инструмента:
- Набор функций для удобных проверок — ассерты (
assert
) и жёсткие проверки (require
). \ - Пакет для создания и настройки моков (
mock
).
Ассерты
Ассерты — это готовые функции, которые проверяют условие и при его нарушении помечают тест как упавший. Они заменяют ручные проверки и делают код лаконичным.
Простейший пример:
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestSum(t *testing.T) {
got := 2 + 2
assert.Equal(t, 4, got) // ожидаем, что результат равен 4
}
Раньше здесь пришлось бы писать условие и выводить сообщение через t.Errorf
. Теперь тест читается как простое утверждение.
Проверки со строками
Ассерты особенно удобны при работе со строками.
func TestHello(t *testing.T) {
msg := "hello" + " world"
assert.Equal(t, "hello world", msg) // строки равны
assert.NotEqual(t, "goodbye", msg) // строки не равны
assert.Contains(t, msg, "world") // строка содержит подстроку
assert.NotContains(t, msg, "cat") // подстрока отсутствует
}
Код остаётся компактным и легко читается.
Проверки ошибок
Вместо ручных сравнений удобно использовать ассерты для ошибок.
import "fmt"
func Divide(a, b int) (int, error) {
if b == 0 {
return 0, fmt.Errorf("division by zero")
}
return a / b, nil
}
func TestDivide(t *testing.T) {
_, err := Divide(10, 0)
assert.Error(t, err) // ошибка есть
assert.EqualError(t, err, "division by zero") // текст ошибки совпадает
}
Результат тот же, но код проще и яснее.
assert и require
В testify есть два вида проверок:
assert
сообщает об ошибке, но позволяет тесту продолжить выполнение.require
останавливает тест сразу, какt.Fatal
.
import "github.com/stretchr/testify/require"
func TestDivideRequire(t *testing.T) {
_, err := Divide(10, 0)
require.Error(t, err) // если ошибки нет — тест не продолжится
}
Часто используют комбинацию: сначала критические условия проверяют через require
, а уточняющие детали — через assert
.
Табличные тесты
Ассерты хорошо подходят для табличных тестов, где проверяется много входных данных.
func Max(a, b int) int {
if a > b {
return a
}
return b
}
func TestMax(t *testing.T) {
cases := []struct {
a, b int
want int
}{
{2, 3, 3},
{10, 5, 10},
{7, 7, 7},
}
for _, c := range cases {
got := Max(c.a, c.b)
assert.Equal(t, c.want, got, "Max(%d, %d)", c.a, c.b)
}
}
Каждая проверка выражается одной строкой, а сообщения об ошибках показывают, для какого случая тест провалился.
Проверки структур и коллекций
Testify умеет сравнивать структуры, срезы и карты без дополнительного кода.
type User struct {
ID int
Name string
}
func TestStructsAndSlices(t *testing.T) {
// сравнение структур
assert.Equal(t, User{1, "Alice"}, User{1, "Alice"})
// сравнение срезов
assert.Equal(t, []int{1, 2, 3}, []int{1, 2, 3})
// если порядок не важен
assert.ElementsMatch(t, []int{3, 2, 1}, []int{1, 2, 3})
}
Есть отдельные функции для проверки JSON, подмножеств и времени (assert.JSONEq
, assert.Subset
, assert.WithinDuration
).
Моки
Вторая часть testify — это пакет mock
, позволяющий создавать поддельные зависимости. Вместо того чтобы писать собственные реализации интерфейсов, можно объявить мок и настраивать его прямо в тесте.
Пример: сервис зависит от хранилища пользователей.
type UserStorage interface {
GetUser(id int) (string, error)
}
type Service struct {
storage UserStorage
}
func NewService(storage UserStorage) *Service {
return &Service{storage: storage}
}
func (s *Service) GetName(id int) (string, error) {
return s.storage.GetUser(id)
}
Мок на testify:
import "github.com/stretchr/testify/mock"
type MockUserStorage struct {
mock.Mock
}
func (m *MockUserStorage) GetUser(id int) (string, error) {
args := m.Called(id)
return args.String(0), args.Error(1)
}
Тест с использованием мока:
func TestServiceWithMock(t *testing.T) {
m := new(MockUserStorage)
// настраиваем поведение: при GetUser(1) вернуть "Alice", nil
m.On("GetUser", 1).Return("Alice", nil)
service := NewService(m)
name, err := service.GetName(1)
require.NoError(t, err)
assert.Equal(t, "Alice", name)
// проверяем, что метод действительно вызывался
m.AssertCalled(t, "GetUser", 1)
m.AssertNumberOfCalls(t, "GetUser", 1)
}
Моки можно настраивать для разных входов и разных сценариев:
m.On("GetUser", 1).Return("Alice", nil)
m.On("GetUser", 2).Return("Bob", nil)
m.On("GetUser", 99).Return("", fmt.Errorf("not found"))
Также есть возможность проверять все ожидаемые вызовы через m.AssertExpectations(t)
.
Библиотека testify решает две основные задачи:
- Ассерты (
assert
иrequire
) делают проверки короче и понятнее. - Моки (
mock
) позволяют удобно подменять зависимости и контролировать их вызовы.
Использование testify делает тесты более выразительными и поддерживаемыми. Вместо длинных условий и повторяющегося кода остаются простые и понятные утверждения, которые показывают суть проверки.
Практика: https://chatgpt.com/canvas/shared/68d9b6ba62b48191a62aa2c289cf7e46
Самостоятельная работа
С помощью testify проверим работу функции, которая формирует URL‑дружелюбный идентификатор из произвольной строки.
Для этого создана функция func Slug(s string) string
, которая нормализует строку и приводит ее к виду kebab-case
ю
- Подключите библиотеку
testify
к проекту (импорт в тесте:github.com/stretchr/testify/assert
) - Напишите табличные тесты на
testify/assert
.
package sluggy
import (
"strings"
"unicode"
)
// Slug нормализует строку для использования в URL.
func Slug(s string) string {
s = strings.ToLower(s)
var b strings.Builder
prevHyphen := false
for _, r := range s {
if unicode.IsLetter(r) || unicode.IsDigit(r) {
b.WriteRune(r)
prevHyphen = false
continue
}
if !prevHyphen {
b.WriteByte('-')
prevHyphen = true
}
}
out := b.String()
out = strings.Trim(out, "-")
// Схлопывание уже обеспечено логикой prevHyphen
return out
}
Что покрыть в тестах
- Пробелы и пунктуация → дефисы.
- Повторяющиеся разделители → один дефис.
- Unicode‑буквы сохраняются и нижний регистр применяется.
- Пустая строка.
Подсказка
- Используйте
assert.Equal(t, want, got)
и под‑тесты черезt.Run
. - Импорт для теста:
github.com/stretchr/testify/assert
.
Показать решение
package sluggy
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestSlug(t *testing.T) {
tests := []struct {
name string
in string
want string
}{
{"basic", "Hello World", "hello-world"},
{"punct", "Go, Dev!", "go-dev"},
{"dupes", "a---b__c", "a-b-c"},
{"unicode", "Привет Мир", "привет-мир"},
{"empty", "", ""},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got := Slug(tc.in)
assert.Equal(t, tc.want, got)
})
}
}
Дополнительные материалы
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.