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

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

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

Начать курс

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

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

Почему Docker важен для интеграционных тестов на Go?

Традиционный подход с моками и заглушками (mocks/stubs) проверяет только логику вашего кода, но не поведение в связке с реальной СУБД или брокером. Docker решает эту проблему:

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

Однако ручное управление контейнерами через os/exec или shell-скрипты быстро становится неустойчивым. На помощь приходят библиотеки, такие как testcontainers-go.

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

testcontainers-go — это Go-обертка, которая программно управляет жизненным циклом Docker-контейнеров. Она автоматически скачивает образы, запускает, ждет готовности и чистит контейнеры после теста.

Установка

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

Базовый пример: тестирование с PostgreSQL

package integration

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 TestPostgresConnection(t *testing.T) {
    ctx := context.Background()

    // 1. Запускаем контейнер PostgreSQL
    pgContainer, err := postgres.RunContainer(ctx,
        testcontainers.WithImage("postgres:15"),
        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) // Автоматическая очистка

    // 2. Получаем строку подключения
    connStr, err := pgContainer.ConnectionString(ctx, "sslmode=disable")
    if err != nil {
        t.Fatal(err)
    }

    // 3. Подключаемся и выполняем запрос
    db, err := sql.Open("postgres", connStr)
    if err != nil {
        t.Fatal(err)
    }
    defer db.Close()

    if err := db.Ping(); err != nil {
        t.Fatal("Не удалось подключиться к БД:", err)
    }

    // Проверяем, что можем создать таблицу и вставить данные
    _, err = db.Exec(`CREATE TABLE IF NOT EXISTS users (id SERIAL PRIMARY KEY, name TEXT)`)
    if err != nil {
        t.Fatal(err)
    }
    t.Log("Интеграционный тест с PostgreSQL пройден успешно!")
}

Продвинутые техники: параллельное выполнение и кастомные ожидания

Когда тестов много, последовательный запуск контейнеров для каждого теста замедляет прогон. Используем параллелизм Go и шаред-контейнеры.

Параллельные тесты с общим контейнером

Создаем один контейнер на весь пакет через TestMain:

package integration

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

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

var db *sql.DB

func TestMain(m *testing.M) {
    ctx := context.Background()

    pgContainer, err := postgres.RunContainer(ctx,
        testcontainers.WithImage("postgres:15"),
        postgres.WithDatabase("testdb"),
        postgres.WithUsername("user"),
        postgres.WithPassword("password"),
    )
    if err != nil {
        os.Exit(1)
    }

    connStr, _ := pgContainer.ConnectionString(ctx, "sslmode=disable")
    db, _ = sql.Open("postgres", connStr)

    code := m.Run()

    db.Close()
    pgContainer.Terminate(ctx)
    os.Exit(code)
}

func TestUserInsert(t *testing.T) {
    t.Parallel()
    _, err := db.Exec("INSERT INTO users (name) VALUES ($1)", "Alice")
    if err != nil {
        t.Fatal(err)
    }
}

func TestUserSelect(t *testing.T) {
    t.Parallel()
    var name string
    err := db.QueryRow("SELECT name FROM users WHERE id = 1").Scan(&name)
    if err != nil {
        t.Fatal(err)
    }
    if name != "Alice" {
        t.Fatalf("ожидалось Alice, получено %s", name)
    }
}

Важно: параллельные тесты, работающие с одной БД, должны быть идемпотентными или использовать изолированные транзакции (rollback после теста).

Кастомные стратегии ожидания (Wait Strategies)

Иногда простого ожидания лога недостаточно. Например, для Redis нужно дождаться ответа PING. Testcontainers позволяет писать свои стратегии:

package integration

import (
    "context"
    "time"

    "github.com/testcontainers/testcontainers-go"
    "github.com/testcontainers/testcontainers-go/wait"
)

type RedisReadyStrategy struct{}

func (s RedisReadyStrategy) WaitUntilReady(ctx context.Context, target wait.StrategyTarget) error {
    // Цикл с проверкой через redis-cli или Go-клиент
    for i := 0; i < 10; i++ {
        // ... выполняем PING
        if ready {
            return nil
        }
        time.Sleep(1 * time.Second)
    }
    return testcontainers.ErrContainerExited
}

Используйте встроенные стратегии: wait.ForLog, wait.ForHTTP, wait.ForSQL — они покрывают 90% случаев.

Интеграция с Docker Compose для сложных сценариев

Когда ваш

Блоги

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