Тестирование: табличные тесты и мокирование интерфейсов
В Node.js вы привыкли к экосистеме Jest, Mocha и Sinon. В Go подход другой: инструменты тестирования встроены в стандартную библиотеку и поставляются вместе с компилятором. Это часть философии языка: код готов только тогда, когда он протестирован.
Основы: go test и объект testing.T
Для тестов не нужно скачивать пакеты. Просто создайте файл с суффиксом _test.go. Компилятор проигнорирует его при сборке приложения, но использует при запуске команды go test.
Правила оформления:
- Функция начинается с префикса
Test. - Принимает аргумент
*testing.T. - Находится в том же пакете, что и основной код.
В Go нет expect или should. Мы используем обычные условия if. Объект testing.T управляет процессом: сообщает об ошибках или останавливает тест.
// math_test.go
func TestSum(t *testing.T) {
result := Sum(2, 2)
expected := 4
if result != expected {
// t.Errorf фиксирует ошибку, но не прерывает выполнение теста
t.Errorf("Sum(2, 2) = %d; want %d", result, expected)
}
}
Табличные тесты (Table-driven tests)
Если нужно проверить функцию на разных входных данных (например, валидацию email), не копируйте функции. Используйте Table-driven tests.
Вы создаете слайс структур со сценариями, итерируетесь по нему и запускаете подтесты через t.Run. Это делает тесты наглядными, как спецификация.
func TestValidateEmail(t *testing.T) {
tests := []struct {
name string
email string
want bool
}{
{"valid email", "user@example.com", true},
{"missing @", "userexample.com", false},
{"empty string", "", false},
{"multiple @", "user@@example.com", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := ValidateEmail(tt.email); got != tt.want {
t.Errorf("ValidateEmail(%s) = %v, want %v", tt.email, got, tt.want)
}
})
}
}
Мокирование через интерфейсы
Юнит-тесты не должны зависеть от реальной базы данных — это долго и нестабильно. Здесь помогает неявная реализация интерфейсов в Go (Duck Typing).
Чтобы подменить реальный объект на «заглушку», достаточно создать структуру, которая реализует те же методы, что и интерфейс репозитория. Mocking в Go — это не магия библиотек, а архитектурный прием.
Как показано на Схеме 1, интерфейс работает как универсальный разъем. Бизнес-логике неважно, что за ним: драйвер PostgreSQL или ваш мок.
Пример ручного мока для сервиса оплаты:
type OrderRepository interface {
GetBalance(userID int) (int, error)
}
type mockRepo struct {
balance int
err error
}
func (m mockRepo) GetBalance(userID int) (int, error) {
return m.balance, m.err
}
func TestProcessPayment(t *testing.T) {
// Настраиваем нужное состояние мока
myMock := mockRepo{balance: 100, err: nil}
// Внедряем мок в сервис
service := NewPaymentService(myMock)
err := service.Pay(1, 50)
if err != nil {
t.Errorf("ожидалась успешная оплата, получена ошибка: %v", err)
}
}
Анализ покрытия (Test coverage)
Инструмент Test coverage встроен в Go и показывает, какие строки кода задействованы в тестах.
- Посмотреть процент в консоли:
go test -cover ./... - Открыть детальный HTML-отчет с подсветкой строк:
go test -coverprofile=cover.out ./... && go tool cover -html=cover.out
💡 Не гонитесь за 100% покрытием. В бэкенде важнее протестировать сложную логику и обработку ошибок, чем простые конструкторы.
Безопасность и конкурентность
Go часто запускает тесты параллельно (через t.Parallel()). Чтобы избежать скрытых багов при работе с памятью, используйте флаг -race. Это активирует Race Detector — он найдет «состояния гонки», когда несколько потоков одновременно меняют одни и те же данные. 🏎️
Теперь вы умеете писать надежный код. В следующем уроке мы упакуем наше приложение в легковесный контейнер и подготовим его к деплою.