Введение: Почему тестирование в Go — это важно?
Go (Golang) — это язык, который с самого проектирования ориентирован на простоту, производительность и надежность. Встроенный пакет testing и мощные инструменты, такие как go test, делают процесс написания тестов неотъемлемой частью разработки. Без качественного тестирования даже самый элегантный код со временем превращается в "хрупкую" систему, где одно изменение ломает всё.
В этой статье мы разберем три ключевых уровня тестирования: unit (модульное), integration (интеграционное) и e2e (end-to-end). Вы узнаете, чем они отличаются, когда их применять и как писать эффективные тесты на Go. Материал будет полезен как новичкам, так и опытным разработчикам, желающим систематизировать знания.
1. Unit-тестирование (Модульное тестирование)
Unit-тесты — это фундамент пирамиды тестирования. Они проверяют работу отдельных функций, методов или модулей изолированно от внешних зависимостей (базы данных, сети, файловой системы).
Основные принципы unit-тестов в Go
- Изоляция: Тестируемый код не должен зависеть от внешних сервисов.
- Быстрота: Unit-тесты выполняются за миллисекунды.
- Детерминированность: Результат теста не должен меняться от запуска к запуску.
Пример простого unit-теста
Допустим, у нас есть функция для сложения двух чисел:
// math.go
package math
func Add(a, b int) int {
return a + b
}
Тест для этой функции будет выглядеть так:
// math_test.go
package math
import "testing"
func TestAdd(t *testing.T) {
result := Add(2, 3)
expected := 5
if result != expected {
t.Errorf("Add(2, 3) = %d; want %d", result, expected)
}
}
Запускается тест командой: go test ./...
Табличные тесты (Table-driven tests)
Go-сообщество предпочитает табличные тесты для проверки множества кейсов:
func TestAddTable(t *testing.T) {
tests := []struct {
a, b, expected int
}{
{1, 1, 2},
{0, 0, 0},
{-1, 1, 0},
{100, 200, 300},
}
for _, tt := range tests {
result := Add(tt.a, tt.b)
if result != tt.expected {
t.Errorf("Add(%d, %d) = %d; want %d", tt.a, tt.b, result, tt.expected)
}
}
}
Совет: Используйте t.Run() для создания вложенных тестов с понятными именами — это улучшает читаемость отчетов.
2. Интеграционное тестирование
Интеграционные тесты проверяют взаимодействие между компонентами системы: база данных, API, внешние сервисы. В отличие от unit-тестов, здесь мы поднимаем реальные (или почти реальные) зависимости.
Особенности интеграционных тестов в Go
- Часто требуют настройки окружения (например, тестовой БД).
- Могут использовать build tags для разделения unit и integration тестов.
- Выполняются медленнее, поэтому их запускают отдельно.
Пример: тестирование работы с БД (PostgreSQL)
Создадим файл с build tag integration:
// db_test.go
//go:build integration
package db
import (
"database/sql"
"testing"
)
func TestSaveUser(t *testing.T) {
// Подключаемся к тестовой БД
dsn := "host=localhost user=test password=test dbname=testdb sslmode=disable"
db, err := sql.Open("postgres", dsn)
if err != nil {
t.Fatalf("failed to connect to db: %v", err)
}
defer db.Close()
// Очищаем данные перед тестом
_, _ = db.Exec("TRUNCATE TABLE users")
// Выполняем тестируемую операцию
user := User{Name: "Alice", Email: "alice@example.com"}
err = SaveUser(db, user)
if err != nil {
t.Errorf("SaveUser() error = %v", err)
}
// Проверяем, что пользователь действительно сохранен
var count int
db.QueryRow("SELECT COUNT(*) FROM users WHERE email = $1", user.Email).Scan(&count)
if count != 1 {
t.Errorf("expected 1 user, got %d", count)
}
}
Запуск: go test -tags=integration ./...
Использование моков и тестовых контейнеров
Для изоляции интеграционных тестов часто используют:
- Mock-объекты — библиотеки
gomockилиtestify/mock. - Testcontainers — для поднятия временных БД, Redis и т.д. в Docker.
Пример с testify/mock:
type MockDB struct {
mock.Mock
}
func (m *MockDB) GetUser(id int) (User, error) {
args := m.Called(id)
return args.Get(0).(User), args.Error(1)
}
3. E2E (End-to-End) тестирование
E2E-тесты проверяют систему целиком, имитируя действия реального пользователя. Они запускаются в окружении, максимально приближенном к production.
Когда нужны e2e-тесты?
- Проверка критических пользовательских сценариев (регистрация, оплата).
- Тестирование интеграции нескольких микросервисов.
- Валидация API Gateway, балансировщиков нагрузки.
Пример e2e-теста для HTTP API
Используем пакет net/http/httptest для создания тестового сервера:
package e2e
import (
"net/http"
"net/http/httptest"
"testing"
)
func TestUserRegistrationFlow(t *testing.T) {
// Создаем тестовый сервер (имитация полного приложения)
handler := setupRouter() // ваша функция инициализации роутера
ts := httptest.NewServer