E2E тестирование Go приложений: от идеи до деплоя без багов

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Почему 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(), но убедитесь

Блоги

Рекомендаций по книгам