Введение: Почему тестирование в Go — это база
Go (Golang) был создан с мыслью о простоте и надёжности. Встроенный пакет testing — один из лучших инструментов для написания тестов среди современных языков. Он не требует сторонних библиотек для базовых сценариев, но при этом легко расширяется. В этой статье мы разберём три уровня тестирования: unit, integration и e2e. Вы узнаете, как их писать, когда применять и какие инструменты использовать. Материал будет полезен как новичкам, так и опытным Go-разработчикам.
1. Unit-тесты в Go: основа основ
Unit-тесты проверяют отдельные функции или методы в изоляции. В Go для этого достаточно создать файл с суффиксом _test.go и написать функции вида TestXxx(t *testing.T).
Простейший пример 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)
}
}
Табличные тесты (Table-Driven Tests)
Go-сообщество рекомендует использовать табличные тесты для проверки множества кейсов:
func TestAddTable(t *testing.T) {
tests := []struct {
name string
a, b int
want int
}{
{"positive", 2, 3, 5},
{"negative", -1, -2, -3},
{"zero", 0, 0, 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Add(tt.a, tt.b); got != tt.want {
t.Errorf("Add() = %d, want %d", got, tt.want)
}
})
}
}
Запускаются unit-тесты командой:
go test ./...
2. Интеграционные тесты: проверяем взаимодействие
Интеграционные тесты проверяют, как разные части системы работают вместе: база данных, API, файловая система. В Go их принято выносить в отдельные файлы с тегом сборки или размещать в пакете integration.
Использование build tags
Создайте файл integration_test.go с тегом:
//go:build integration
package integration
import (
"database/sql"
"testing"
_ "github.com/lib/pq"
)
func TestDatabaseConnection(t *testing.T) {
dsn := "host=localhost port=5432 user=test dbname=test sslmode=disable"
db, err := sql.Open("postgres", dsn)
if err != nil {
t.Fatalf("Failed to open DB: %v", err)
}
defer db.Close()
err = db.Ping()
if err != nil {
t.Fatalf("Failed to ping DB: %v", err)
}
}
Запуск только интеграционных тестов:
go test -tags=integration ./...
Работа с тестовой БД
Для интеграционных тестов часто используют testcontainers-go или in-memory БД (например, SQLite). Пример с testcontainers:
func TestWithPostgresContainer(t *testing.T) {
ctx := context.Background()
pgContainer, err := postgres.RunContainer(ctx,
testcontainers.WithImage("postgres:15"),
postgres.WithDatabase("testdb"),
)
require.NoError(t, err)
defer pgContainer.Terminate(ctx)
dsn, _ := pgContainer.ConnectionString(ctx)
// используем dsn для подключения
}
3. E2E-тесты: проверка системы целиком
End-to-End тесты симулируют поведение пользователя и проверяют всю систему от начала до конца. В Go их часто пишут для HTTP-сервисов с использованием пакета net/http/httptest.
Пример E2E-теста для REST API
func TestCreateUserE2E(t *testing.T) {
// Запускаем тестовый сервер
router := setupRouter() // ваша функция инициализации роутера
ts := httptest.NewServer(router)
defer ts.Close()
// Отправляем запрос
payload := `{"name":"John","email":"john@example.com"}`
resp, err := http.Post(ts.URL+"/users", "application/json",
strings.NewReader(payload))
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
// Проверяем статус
if resp.StatusCode != http.StatusCreated {
t.Errorf("Expected status 201, got %d", resp.StatusCode)
}
}
Мокирование внешних сервисов
Для E2E-тестов часто нужно изолировать внешние зависимости. Используйте библиотеку gomock или testify/mock:
type MockPaymentService struct {
mock.Mock
}
func (m *MockPaymentService) Charge(amount float64) error {
args := m.Called(amount)
return args.Error(0)
}
func TestOrderWithMock(t *testing.T) {
mockSvc := new(MockPaymentService)
mockSvc.On("Charge", 100.0).Return(nil)
order := NewOrder(mockSvc)
err := order.Process(100.0)
assert.NoError(t, err)
mockSvc.AssertExpectations(t)
}
4. Best Practices и полезные инструменты
- Именование: используйте осмысленные имена тестов, например
TestUserCreate_ValidInput_Success. - Изоляция: каждый тест должен быть независимым. Используйте
t.Parallel()для ускорения. - Покрытие: запускайте
go test -cover, чтобы видеть процент покрытия кода. - Фикстуры: для сложных данных используйте
testdataпапку или библиотекуgo-faker. - CI/CD: интеграционные и E2E-тесты запускайте только в CI-пайплайне, а unit-тесты — при каждом коммите.
Популярные библиотеки для тестирования в Go:
testify— assert, mock, suite