Тестирование в Go: Unit, Integration, E2E — полное руководство
Тестирование — неотъемлемая часть разработки качественного программного обеспечения. В языке Go (Golang) тестирование заложено на уровне стандартной библиотеки, что делает его простым и эффективным. В этой статье мы разберем три основных уровня тестирования: модульные (unit), интеграционные (integration) и сквозные (e2e) тесты. Вы узнаете, как их писать, когда применять и какие инструменты использовать.
Почему тестирование в Go так важно?
Go был создан с акцентом на простоту и надежность. Встроенный пакет testing и инструмент go test позволяют легко добавлять тесты в любой проект. Команда Go придерживается философии: "Если код не протестирован, он не работает". Тестирование помогает:
- Обнаруживать ошибки на ранних стадиях разработки
- Упрощать рефакторинг и поддержку кода
- Документировать поведение функций и модулей
- Повышать уверенность в стабильности приложения
Unit-тестирование в Go
Модульные тесты (unit-тесты) проверяют работу отдельных функций, методов или структур изолированно от внешних зависимостей. В Go они пишутся в файлах с суффиксом _test.go.
Основы unit-тестов
Рассмотрим простой пример. Допустим, у нас есть функция сложения:
// math.go
package main
func Add(a, b int) int {
return a + b
}
Тест для этой функции будет выглядеть так:
// math_test.go
package main
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 -v
Табличные тесты (Table-driven tests)
Go поощряет использование табличных тестов для проверки множества сценариев:
func TestAddTableDriven(t *testing.T) {
tests := []struct {
name string
a, b int
want int
}{
{"positive numbers", 2, 3, 5},
{"negative numbers", -1, -2, -3},
{"zero", 0, 0, 0},
{"mixed", -5, 10, 5},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Add(tt.a, tt.b)
if got != tt.want {
t.Errorf("Add(%d, %d) = %d; want %d", tt.a, tt.b, got, tt.want)
}
})
}
}
Тестирование с моками (Mock)
Для изоляции unit-тестов часто требуются моки. В Go можно использовать интерфейсы и библиотеки, такие как gomock или testify/mock.
// Определяем интерфейс
type UserRepository interface {
GetUser(id int) (User, error)
}
// Тест с использованием testify
func TestGetUserName(t *testing.T) {
mockRepo := new(MockUserRepository)
mockRepo.On("GetUser", 1).Return(User{Name: "Alice"}, nil)
service := NewUserService(mockRepo)
name, err := service.GetUserName(1)
assert.NoError(t, err)
assert.Equal(t, "Alice", name)
mockRepo.AssertExpectations(t)
}
Интеграционное тестирование в Go
Интеграционные тесты проверяют взаимодействие между компонентами системы: базами данных, API, файловыми системами и другими внешними сервисами. В Go принято выносить их в отдельные пакеты или использовать build tags.
Тестирование с базой данных
Для тестирования работы с БД часто используют testcontainers или временные базы данных:
// integration_test.go (с build tag)
//go:build integration
package integration
import (
"database/sql"
"testing"
_ "github.com/lib/pq"
)
func TestUserRepository(t *testing.T) {
// Подключение к тестовой БД
db, err := sql.Open("postgres", "host=localhost port=5432 user=test password=test dbname=testdb sslmode=disable")
if err != nil {
t.Fatalf("failed to connect: %v", err)
}
defer db.Close()
repo := NewUserRepository(db)
user, err := repo.CreateUser("test@example.com")
if err != nil {
t.Fatalf("failed to create user: %v", err)
}
if user.Email != "test@example.com" {
t.Errorf("expected email test@example.com, got %s", user.Email)
}
}
Запуск интеграционных тестов: go test -tags=integration -v ./...
Тестирование HTTP-сервера
Пакет net/http/httptest позволяет тестировать HTTP-обработчики без запуска реального сервера:
func TestHandler(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"message": "hello"}`))
})
req := httptest.NewRequest("GET", "/api/hello", nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
resp := w.Result()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
t.Errorf("expected status 200, got %d", resp.StatusCode)
}
expected := `{"message": "hello"}`
if string(body) != expected {
t.Errorf("expected body %s, got %s", expected, string(body))
}
}
E2E-тестирование в Go
Сквозные тесты (end-to-end) проверяют работу приложения целиком, включая все внешние зависимости. Они имитируют действия реального пользователя и обычно запускаются в CI/CD после unit и integration тестов.
Пример E2E-теста для REST API
// e2e_test.go
//go:build e2e
package e2e
import (
"net/http"
"testing"
"time"
)
func TestFullUserFlow(t *testing.T) {
// Предполагаем, что сервер уже запущен
baseURL := "http://localhost:8080"
client := &http.Client{Timeout: 10 * time.Second}
// 1. Создание пользователя
resp, err := client.Post(baseURL+"/api/users", "application/json",
strings.NewReader(`{"email":"test@test.com","name":"Test User"}`))
if err != nil {