Почему E2E тестирование — это ваш страховой полис
Представьте: вы написали крутой микросервис на Go. Юнит-тесты зеленые, интеграционные — тоже. Выкатываете в прод, и… пользователи жалуются, что корзина не очищается после оплаты. Знакомо? Проблема в том, что E2E тестирование Go приложений часто остается «на потом». А зря.
Сквозные тесты (End-to-End) — это единственный способ проверить, что ваша система работает как единое целое: от HTTP-запроса до записи в базу и обратно. В этой статье мы не просто поговорим о теории — я покажу вам реальные примеры кода на Go, которые можно скопировать и запустить уже сегодня.
Что такое E2E тестирование в контексте Golang?
E2E тестирование Go — это проверка полного пользовательского сценария: старт приложения, выполнение бизнес-логики, взаимодействие с внешними сервисами (БД, кэш, очереди) и проверка результата. В отличие от unit-тестов, мы не мокаем базу данных — мы поднимаем реальную PostgreSQL в контейнере.
Чем E2E отличается от интеграционных тестов?
Часто эти понятия путают. Давайте расставим точки над i:
- Unit-тесты — проверяют одну функцию. Быстро, изолированно.
- Интеграционные тесты — проверяют связку модулей (например, репозиторий + БД).
- E2E тесты — поднимают всё приложение целиком и имитируют действия реального пользователя.
Для сквозного тестирования Golang мы будем использовать стандартный пакет testing плюс мощные утилиты: testcontainers-go и dockertest.
Настройка окружения: Docker Compose и Testcontainers
Самый простой способ организовать e2e тестирование go — использовать Docker. У вас есть два пути:
Вариант 1: Docker Compose (для CI/CD)
Создайте файл docker-compose.test.yml:
version: '3.8'
services:
db:
image: postgres:15-alpine
environment:
POSTGRES_DB: testdb
POSTGRES_USER: test
POSTGRES_PASSWORD: test
ports:
- "5432:5432"
app:
build: .
depends_on:
- db
environment:
DATABASE_URL: "postgres://test:test@db:5432/testdb?sslmode=disable"
ports:
- "8080:8080"
Вариант 2: Testcontainers (для локальной разработки)
Библиотека testcontainers-go позволяет поднимать контейнеры прямо из тестов. Это мой фаворит — тесты сами управляют инфраструктурой.
// setup_test.go
package e2e_test
import (
"context"
"testing"
"time"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
)
func setupPostgres(t *testing.T) string {
t.Helper()
ctx := context.Background()
req := testcontainers.ContainerRequest{
Image: "postgres:15-alpine",
ExposedPorts: []string{"5432/tcp"},
Env: map[string]string{
"POSTGRES_DB": "testdb",
"POSTGRES_USER": "test",
"POSTGRES_PASSWORD": "test",
},
WaitingFor: wait.ForLog("database system is ready to accept connections").
WithStartupTimeout(30 * time.Second),
}
container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
if err != nil {
t.Fatalf("failed to start container: %v", err)
}
t.Cleanup(func() {
container.Terminate(ctx)
})
host, _ := container.Host(ctx)
port, _ := container.MappedPort(ctx, "5432")
return "postgres://test:test@" + host + ":" + port.Port() + "/testdb?sslmode=disable"
}
Пишем первый E2E тест для HTTP-сервера на Go
Допустим, у нас есть простой API для управления задачами. Напишем e2e тест golang, который проверит полный цикл: создание задачи, получение по ID и удаление.
// todo_e2e_test.go
package e2e_test
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/your-project/app"
"github.com/your-project/models"
)
func TestTodoE2E(t *testing.T) {
// 1. Поднимаем БД через testcontainers
dsn := setupPostgres(t)
// 2. Инициализируем приложение с реальной БД
server := app.NewServer(app.Config{
DatabaseURL: dsn,
})
// 3. Запускаем сервер на случайном порту
ts := httptest.NewServer(server.Handler)
defer ts.Close()
// 4. Тест: создаем задачу
todo := models.Todo{Title: "Купить молоко", Completed: false}
body, _ := json.Marshal(todo)
resp, err := http.Post(ts.URL+"/api/todos", "application/json", bytes.NewReader(body))
if err != nil {
t.Fatalf("failed to create todo: %v", err)
}
if resp.StatusCode != http.StatusCreated {
t.Fatalf("expected 201, got %d", resp.StatusCode)
}
var created models.Todo
json.NewDecoder(resp.Body).Decode(&created)
resp.Body.Close()
if created.ID == 0 {
t.Error("expected non-zero ID")
}
// 5. Тест: получаем задачу по ID
resp, err = http.Get(ts.URL + "/api/todos/" + created.ID)
if err != nil {
t.Fatalf("failed to get todo: %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Fatalf("expected 200, got %d", resp.StatusCode)
}
// 6. Проверяем, что данные сохранились
var fetched models.Todo
json.NewDecoder(resp.Body).Decode(&fetched)
resp.Body.Close()
if fetched.Title != "Купить молоко" {
t.Errorf("expected 'Купить молоко', got '%s'", fetched.Title)
}
}
Best practices для E2E тестирования Go приложений
За годы работы с тестированием Golang я выработал несколько правил, которые спасают нервы:
1. Изолируйте тестовые данные
Каждый тест должен начинаться с чистого состояния. Используйте транзакции с откатом или отдельные схемы БД.
func TestCleanState(t *testing.T) {
// Открываем транзакцию
tx := db.Begin()
defer tx.Rollback()
// Все операции внутри транзакции
// После Rollback данные не сохраняются
}
2. Параллельный запуск — это норма
Go тесты отлично работают параллельно. Используйте t.Parallel(), но убедитесь