Введение: Почему тестирование в Go — это необходимость?
Go (Golang) — это современный язык программирования, созданный для построения надёжных и масштабируемых систем. Однако даже самый элегантный код не застрахован от ошибок. Именно поэтому тестирование является неотъемлемой частью разработки на Go. Встроенный пакет testing предоставляет мощный инструментарий для написания юнит-тестов, интеграционных тестов и даже end-to-end (e2e) проверок.
В этой статье мы разберём все уровни тестирования в Go: от простых unit-тестов до комплексных e2e-сценариев. Вы узнаете, как правильно структурировать тесты, использовать моки, писать table-driven тесты и настраивать интеграционное тестирование с реальными зависимостями. Материал будет полезен как новичкам, так и опытным разработчикам, желающим систематизировать знания.
1. Unit-тестирование в Go: основа основ
Unit-тесты проверяют работу отдельных функций или методов в изоляции от внешних зависимостей. В Go для этого используется стандартный пакет testing. Основные правила: файлы тестов должны называться *_test.go, а функции тестов — начинаться с Test.
1.1. Простой 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 ./... или go test -v для подробного вывода.
1.2. Table-driven тесты (табличные тесты)
Go-сообщество активно использует table-driven подход для проверки множества кейсов:
func TestAddTableDriven(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)
}
})
}
}
Такой подход делает тесты легко читаемыми и расширяемыми.
1.3. Тестирование с ошибками
Для функций, возвращающих ошибки, используйте t.Fatal или t.Errorf:
func TestDivide(t *testing.T) {
_, err := Divide(10, 0)
if err == nil {
t.Fatal("expected error for division by zero")
}
}
2. Интеграционное тестирование: работа с зависимостями
Интеграционные тесты проверяют взаимодействие компонентов: базы данных, API, файловой системы. В Go их принято отделять от unit-тестов с помощью build-тегов или отдельной структуры пакетов.
2.1. Использование build-тегов
Создайте файл с тегом //go:build integration в начале:
//go:build integration
package db
import (
"testing"
"database/sql"
_ "github.com/lib/pq"
)
func TestPostgresConnection(t *testing.T) {
dsn := "host=localhost port=5432 user=test password=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()
if err := db.Ping(); err != nil {
t.Fatalf("failed to ping db: %v", err)
}
}
Запуск только интеграционных тестов: go test -tags=integration ./...
2.2. Моки и заглушки
Для изоляции тестов используйте интерфейсы и моки. Пример с популярной библиотекой gomock:
// user.go
type UserRepository interface {
GetUser(id int) (*User, error)
}
type UserService struct {
repo UserRepository
}
func (s *UserService) GetUserName(id int) (string, error) {
user, err := s.repo.GetUser(id)
if err != nil {
return "", err
}
return user.Name, nil
}
// user_test.go (с использованием gomock)
func TestGetUserName(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockRepo := NewMockUserRepository(ctrl)
mockRepo.EXPECT().GetUser(1).Return(&User{Name: "Alice"}, nil)
service := &UserService{repo: mockRepo}
name, err := service.GetUserName(1)
if err != nil || name != "Alice" {
t.Errorf("unexpected result: %s, err: %v", name, err)
}
}
3. E2E-тестирование: проверка системы целиком
End-to-end тесты имитируют поведение пользователя и проверяют работу всего приложения от начала до конца. В Go для этого часто используют HTTP-тесты или запуск Docker-контейнеров.
3.1. HTTP-тесты с httptest
Стандартный пакет net/http/httptest позволяет тестировать HTTP-обработчики без запуска реального сервера:
func TestUserHandler(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"name":"Alice"}`))
})
req := httptest.NewRequest("GET", "/user/1", nil)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("expected 200, got %d", rec.Code)
}
expected := `{"name":"Alice"}`
if rec.Body.String() != expected {
t.Errorf("expected %s, got %s", expected, rec.Body.String())
}
}
3.2. Запуск тестовой инфраструктуры
Для полноценных e2e-тестов с БД и сервисами используйте testcontainers-go:
import (
"context"
"testing"
"github.com/testcontainers/testcontainers