Интеграционное тестирование Go с Docker: продвинутый уровень

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

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

Начать курс

Интеграционное тестирование Go с Docker: от теории к продвинутой практике

Интеграционное тестирование — критически важный этап разработки микросервисов на Go. Без проверки взаимодействия с базами данных, очередями сообщений и внешними API вы рискуете получить "работает на моей машине" в самом неприятном его проявлении. Docker позволяет поднимать чистые, изолированные окружения для каждого теста, а продвинутые техники — делать это быстро, параллельно и без боли. В этой статье мы разберём реальные подходы, которые используют senior-инженеры.

1. Почему Docker — идеальный партнёр для интеграционных тестов в Go

Go компилируется в статический бинарник, что делает его идеальным кандидатом для контейнеризации. При интеграционном тестировании Docker решает три ключевые задачи:

  • Изоляция: каждый тест получает свежую базу данных или Redis без "мусора" от предыдущих запусков.
  • Воспроизводимость: тесты работают одинаково на машине разработчика, в CI/CD и на production.
  • Скорость: правильно настроенные контейнеры запускаются за секунды, а не минуты.

Основная ошибка новичков — запуск одного контейнера на все тесты. Это приводит к race conditions и "плавающим" багам. Продвинутый подход: один контейнер на тестовый пакет или даже на отдельный тест.

2. Testcontainers для Go: управляемые контейнеры из кода

Библиотека testcontainers-go позволяет управлять Docker-контейнерами прямо из Go-тестов. Это элегантнее, чем писать bash-скрипты или полагаться на предварительно запущенный Docker Compose.

Установка:

go get github.com/testcontainers/testcontainers-go
go get github.com/testcontainers/testcontainers-go/modules/postgres

Пример: тестирование PostgreSQL с миграциями:

package integration_test

import (
    "context"
    "database/sql"
    "testing"
    "time"

    "github.com/testcontainers/testcontainers-go"
    "github.com/testcontainers/testcontainers-go/modules/postgres"
    "github.com/testcontainers/testcontainers-go/wait"
    _ "github.com/lib/pq"
)

func TestUserRepository(t *testing.T) {
    ctx := context.Background()

    pgContainer, err := postgres.RunContainer(ctx,
        testcontainers.WithImage("postgres:16-alpine"),
        postgres.WithDatabase("testdb"),
        postgres.WithUsername("user"),
        postgres.WithPassword("password"),
        testcontainers.WithWaitStrategy(
            wait.ForLog("database system is ready to accept connections").
                WithOccurrence(2).
                WithStartupTimeout(30*time.Second),
        ),
    )
    if err != nil {
        t.Fatal(err)
    }
    defer pgContainer.Terminate(ctx)

    connStr, err := pgContainer.ConnectionString(ctx, "sslmode=disable")
    if err != nil {
        t.Fatal(err)
    }

    db, err := sql.Open("postgres", connStr)
    if err != nil {
        t.Fatal(err)
    }
    defer db.Close()

    // Здесь выполняем миграции и тесты
    _, err = db.Exec(`CREATE TABLE IF NOT EXISTS users (
        id SERIAL PRIMARY KEY,
        name TEXT NOT NULL
    )`)
    if err != nil {
        t.Fatal(err)
    }

    // Тестируем репозиторий
    repo := NewUserRepository(db)
    user, err := repo.Create(ctx, "Alice")
    if err != nil {
        t.Fatal(err)
    }
    if user.Name != "Alice" {
        t.Errorf("expected Alice, got %s", user.Name)
    }
}

Ключевые моменты:

  • WithWaitStrategy — ждём, пока PostgreSQL действительно будет готов, а не просто контейнер запущен.
  • defer pgContainer.Terminate(ctx) — гарантирует очистку даже при панике.
  • Каждый тест получает свой экземпляр БД — полная изоляция.

3. Параллельные тесты и управление ресурсами

Go поддерживает параллельное выполнение тестов через t.Parallel(). Но с Docker-контейнерами это требует аккуратности: если вы запустите 10 тестов, каждый поднимет свой PostgreSQL, память закончится быстро.

Оптимальная стратегия — пул контейнеров. Используем sync.Pool для переиспользования:

package testutil

import (
    "context"
    "sync"

    "github.com/testcontainers/testcontainers-go"
    "github.com/testcontainers/testcontainers-go/modules/redis"
)

type RedisPool struct {
    pool sync.Pool
}

func NewRedisPool() *RedisPool {
    return &RedisPool{
        pool: sync.Pool{
            New: func() interface{} {
                ctx := context.Background()
                container, err := redis.RunContainer(ctx,
                    testcontainers.WithImage("redis:7-alpine"),
                )
                if err != nil {
                    panic(err)
                }
                return container
            },
        },
    }
}

func (p *RedisPool) Get(ctx context.Context) (*redis.RedisContainer, error) {
    container := p.pool.Get().(*redis.RedisContainer)
    // Проверяем, жив ли контейнер
    if err := container.IsRunning(ctx); err != nil {
        p.pool.Put(container)
        return p.Get(ctx) // рекурсивно получаем новый
    }
    return container, nil
}

func (p *RedisPool) Put(container *redis.RedisContainer) {
    p.pool.Put(container)
}

Использование в тестах:

var redisPool = testutil.NewRedisPool()

func TestRedisCache(t *testing.T) {
    t.Parallel()
    ctx := context.Background()

    container, err := redisPool.Get(ctx)
    if err != nil {
        t.Fatal(err)
    }
    defer redisPool.Put(container)

    // Тестируем кэш
    addr, _ := container.Endpoint(ctx, "redis://")
    cache := NewCache(addr)
    err = cache.Set(ctx, "key", "value", time.Minute)
    if err != nil {
        t.Fatal(err)
    }
}

Важно: синхронизируйте доступ к пулу через мьютексы, если используете его из разных тестовых пакетов. Для production-пайплайнов рассмотрите dockertest от Oracle — он умнее управляет ресурсами.

4. Docker Compose для сложных сценариев

Когда ваше приложение зависит от нескольких сервисов (БД + Redis + Kafka), удобнее использовать Docker Compose. Создайте docker-compose.test.yml:

version: "3.9"
services:
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: testdb
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password
    healthcheck:
      test:                

Блоги

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