testcontainers-go для Go: полное руководство с примерами

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

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

Начать курс

testcontainers-go: Docker в тестах на Go

В современной разработке программного обеспечения тестирование интеграций с внешними сервисами (базами данных, брокерами сообщений, кэшами) является одной из самых сложных задач. Традиционные подходы, такие как использование общих тестовых баз данных или моков, часто приводят к проблемам: тесты становятся хрупкими, зависят от окружения или не отражают реального поведения системы. Библиотека testcontainers-go предлагает элегантное решение: она позволяет запускать изолированные Docker-контейнеры прямо из тестового кода на Go, обеспечивая чистую, воспроизводимую и близкую к production среду для каждого тестового запуска.

Основная идея testcontainers-go заключается в том, чтобы управлять жизненным циклом Docker-контейнеров программно. Вы описываете необходимый сервис (например, PostgreSQL, Redis, Kafka) в коде теста, библиотека автоматически скачивает образ (если его нет), запускает контейнер, дожидается его готовности и предоставляет вам параметры подключения (хост, порт, учетные данные). После завершения теста контейнер автоматически останавливается и удаляется, не оставляя мусора в системе. Это делает тесты независимыми, быстрыми и безопасными.

testcontainers-go — это Go-реализация популярного проекта Testcontainers, который изначально появился в Java, а затем был портирован на другие языки. Библиотека активно поддерживается сообществом и предоставляет готовые модули для большинства популярных сервисов: PostgreSQL, MySQL, Redis, MongoDB, Kafka, RabbitMQ, Elasticsearch и многие другие. Она идеально подходит для написания интеграционных тестов, end-to-end тестов и даже для локальной разработки, когда нужно быстро поднять окружение.

Установка

Для установки testcontainers-go используйте стандартный менеджер пакетов Go. Убедитесь, что у вас установлен Docker (или Podman) и Go версии 1.19 или выше.

go get github.com/testcontainers/testcontainers-go

Для работы с конкретными сервисами (например, PostgreSQL) потребуется установить соответствующий модуль:

go get github.com/testcontainers/testcontainers-go/modules/postgres
go get github.com/testcontainers/testcontainers-go/modules/redis
go get github.com/testcontainers/testcontainers-go/modules/mongodb
go get github.com/testcontainers/testcontainers-go/modules/kafka
# и т.д.

После установки импортируйте пакеты в ваш тестовый файл:

import (
    "context"
    "testing"
    
    "github.com/testcontainers/testcontainers-go"
    "github.com/testcontainers/testcontainers-go/wait"
    "github.com/testcontainers/testcontainers-go/modules/postgres"
)

Быстрый старт: минимальный рабочий пример

Давайте напишем простой тест, который запускает контейнер PostgreSQL, создает таблицу, вставляет данные и проверяет их. Это покажет вам основные принципы работы с библиотекой.

package main

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" // драйвер PostgreSQL
)

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

    // 1. Запускаем контейнер PostgreSQL
    postgresContainer, err := postgres.RunContainer(ctx,
        testcontainers.WithImage("postgres:16-alpine"),
        postgres.WithDatabase("testdb"),
        postgres.WithUsername("testuser"),
        postgres.WithPassword("testpass"),
        testcontainers.WithWaitStrategy(
            wait.ForLog("database system is ready to accept connections").
                WithOccurrence(2).
                WithStartupTimeout(60*time.Second)),
    )
    if err != nil {
        t.Fatal(err)
    }
    defer postgresContainer.Terminate(ctx) // обязательно очищаем контейнер

    // 2. Получаем строку подключения
    connStr, err := postgresContainer.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()

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

    _, err = db.Exec("INSERT INTO users (name) VALUES ($1)", "Alice")
    if err != nil {
        t.Fatal(err)
    }

    var name string
    err = db.QueryRow("SELECT name FROM users WHERE id = $1", 1).Scan(&name)
    if err != nil {
        t.Fatal(err)
    }

    if name != "Alice" {
        t.Errorf("expected 'Alice', got '%s'", name)
    }

    t.Log("Integration test passed!")
}

Запустите тест командой go test -v. Вы увидите, как Docker скачивает образ PostgreSQL (если его нет), запускает контейнер, и тест выполняется успешно. После завершения контейнер будет автоматически удален.

Основные методы и функции

testcontainers-go предоставляет богатый API для управления контейнерами. Ниже приведены 10 ключевых методов и функций с примерами.

1. testcontainers.GenericContainer(ctx, req)

Сигнатура: func GenericContainer(ctx context.Context, req GenericContainerRequest) (Container, error)

Что делает: Создает и запускает контейнер на основе общих параметров. Это самый гибкий метод, позволяющий настроить образ, порты, переменные окружения и стратегии ожидания.

req := testcontainers.ContainerRequest{
    Image:        "nginx:alpine",
    ExposedPorts: []string{"80/tcp"},
    WaitingFor:   wait.ForHTTP("/").WithPort("80/tcp"),
}
container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
    ContainerRequest: req,
    Started:          true,
})
if err != nil {
    log.Fatal(err)
}
defer container.Terminate(ctx)

2. container.Endpoint(ctx, port)

Сигнатура: func (c *DockerContainer) Endpoint(ctx context.Context, port string) (string, error)

Что делает: Возвращает внешний адрес (хост и порт) для доступа к контейнеру. Полезно, когда нужно получить динамически назначенный порт.

endpoint, err := container.Endpoint(ctx, "5432")
if err != nil {
    log.Fatal(err)
}
// endpoint будет выглядеть как "localhost:54321"

3. container.PortEndpoint(ctx, port, scheme)

Сигнатура: func (c *DockerContainer) PortEndpoint(ctx context.Context, port string, scheme string) (string, error)

Что делает: Аналогичен Endpoint, но добавляет схему (например, "http://" или "tcp://") к возвращаемому адресу.

httpEndpoint, err := container.PortEndpoint(ctx, "8080", "http")
// httpEndpoint будет "http://localhost:32768"

4. container.Exec(ctx, cmd)

Сигнатура: func (c *DockerContainer) Exec(ctx context.Context, cmd []string) (int, io.Reader, error)

Что делает: Выполняет команду внутри запущенного контейнера. Возвращает код возврата, stdout и stderr. Полезно для выполнения миграций или проверки состояния.

exitCode, reader, err := container.Exec(ctx, []string{"ls", "-la", "/app"})
if err != nil {
    log.Fatal(err)
}
output, _ := io.ReadAll(reader)
log.Printf("Exit code: %d, Output: %s", exitCode, output)

5. container.CopyFileToContainer(ctx, hostFilePath, containerFilePath, fileMode)

Сигнатура: func (c *DockerContainer) CopyFileToContainer(ctx context.Context, hostFilePath string, containerFilePath string, fileMode int64) error

Что делает: Копирует файл с хост-машины в контейнер. Полезно для загрузки конфигурационных файлов или тестовых данных.

err := container.CopyFileToContainer(ctx, "./testdata/config.yaml", "/app/config.yaml", 0644)
if err != nil {
    log.Fatal(err)
}

6. container.CopyFileFromContainer(ctx, containerFilePath)

Сигнатура: func (c *DockerContainer) CopyFileFromContainer(ctx context.Context, containerFilePath string) (io.ReadCloser, error)

Что делает: Копирует файл из контейнера на хост-машину (возвращает reader). Полезно для извлечения логов или результатов работы.

reader, err := container.CopyFileFromContainer(ctx, "/var/log/app.log")
if err != nil {
    log.Fatal(err)
}
defer reader.Close()
logContent, _ := io.ReadAll(reader)
log.Printf("Log: %s", logContent)

7. wait.ForLog(logString)

Сигнатура: func ForLog(logString string) *LogStrategy

Что делает: Создает стратегию ожидания, которая ждет появления указанной строки в логах контейнера. Это один из самых надежных способов дождаться готовности сервиса.

req := testcontainers.ContainerRequest{
    Image:      "redis:7-alpine",
    WaitingFor: wait.ForLog("Ready to accept connections"),
}

8. wait.ForHTTP(path)

Сигнатура: func ForHTTP(path string) *HTTPStrategy

Что делает: Создает стратегию ожидания, которая выполняет HTTP GET запрос к указанному пути и ждет ответа 200 OK. Идеально для веб-сервисов.

req := testcontainers.ContainerRequest{
    Image:      "nginx:alpine",
    ExposedPorts: []string{"80/tcp"},
    WaitingFor: wait.ForHTTP("/health").WithPort("80/tcp"),
}

9. wait.ForSQL(driver, url, query)

Сигнатура: func ForSQL(driver string, url string, query string) *SQLStrategy

Что делает: Создает стратегию ожидания, которая подключается к базе данных и выполняет проверочный SQL-запрос. Это самый надежный способ дождаться готовности СУБД.

req := testcontainers.ContainerRequest{
    Image:      "postgres:16-alpine",
    Env: map[string]string{
        "POSTGRES_USER": "test",
        "POSTGRES_PASSWORD": "test",
        "POSTGRES_DB": "testdb",
    },
    ExposedPorts: []string{"5432/tcp"},
    WaitingFor: wait.ForSQL("postgres", "host=localhost port=5432 user=test password=test dbname=testdb sslmode=disable", "SELECT 1"),
}

10. testcontainers.WithWaitStrategy(strategy)

Сигнатура: func WithWaitStrategy(strategy wait.Strategy) ContainerOption

Что делает: Функция-опция, которая добавляет стратегию ожидания к запросу контейнера. Может комбинироваться с другими опциями.

container, err := postgres.RunContainer(ctx,
    testcontainers.WithImage("postgres:16-alpine"),
    testcontainers.WithWaitStrategy(
        wait.ForLog("database system is ready to accept connections").
            WithStartupTimeout(120*time.Second),
    ),
)

Реальный практический пример: Тестирование REST API с PostgreSQL и Redis

Давайте создадим полноценный пример, который тестирует REST API, использующее PostgreSQL для хранения данных и Redis для кэширования. Мы напишем тест, который запускает оба контейнера, выполняет HTTP-запросы к нашему API и проверяет корректность ответов.

package integration

import (
    "bytes"
    "context"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "testing"
    "time"

    "github.com/go-redis/redis/v8"
    "github.com/testcontainers/testcontainers-go"
    "github.com/testcontainers/testcontainers-go/modules/postgres"
    "github.com/testcontainers/testcontainers-go/modules/redis"
    "github.com/testcontainers/testcontainers-go/wait"
    "gorm.io/driver/postgres"
    "gorm.io/gorm"
)

// User - модель данных
type User struct {
    ID   uint   `json:"id" gorm:"primaryKey"`
    Name string `json:"name"`
    Age  int    `json:"age"`
}

// App - структура нашего приложения
type App struct {
    DB    *gorm.DB
    Redis *redis.Client
}

func (a *App) GetUser(id uint) (*User, error) {
    // Пытаемся получить из кэша
    val, err := a.Redis.Get(context.Background(), fmt.Sprintf("user:%d", id)).Result()
    if err == nil {
        var user User
        json.Unmarshal([]byte(val), &user)
        return &user, nil
    }

    // Если нет в кэше, идем в БД
    var user User
    if err := a.DB.First(&user, id).Error; err != nil {
        return nil, err
    }

    // Сохраняем в кэш
    data, _ := json.Marshal(user)
    a.Redis.Set(context.Background(), fmt.Sprintf("user:%d", id), data, 10*time.Minute)

    return &user, nil
}

func (a *App) CreateUser(name string, age int) (*User, error) {
    user := User{Name: name, Age: age}
    if err := a.DB.Create(&user).Error; err != nil {
        return nil, err
    }
    return &user, nil
}

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

    // 1. Запускаем PostgreSQL
    postgresContainer, err := postgres.RunContainer(ctx,                    

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