Интеграционное тестирование Go с Docker: полное руководство
Интеграционное тестирование — ключевой этап разработки надёжных Go-приложений. В отличие от юнит-тестов, интеграционные проверяют взаимодействие компонентов: базы данных, кэша, внешних API. Docker позволяет изолировать тестовое окружение, делая тесты воспроизводимыми и независимыми от хост-машины. В этой статье мы разберём, как настроить интеграционное тестирование Go-приложений с Docker — от простых контейнеров до продвинутых сценариев с Testcontainers и CI/CD.
1. Почему Docker для интеграционного тестирования Go?
Go-приложения часто работают с PostgreSQL, Redis, MongoDB или другими сервисами. Ручная настройка этих зависимостей на каждой машине разработчика и CI-сервере — путь к ошибкам. Docker решает эту проблему:
- Воспроизводимость — одинаковое окружение локально и в CI.
- Изоляция — тесты не засоряют хост-систему.
- Скорость — контейнеры запускаются за секунды.
- Параллелизм — можно запускать несколько изолированных тестов одновременно.
Рассмотрим базовый пример: тестируем функцию, которая сохраняет данные в PostgreSQL.
2. Настройка тестового окружения с Docker Compose
Создадим docker-compose.test.yml для тестовой базы данных:
version: '3.8'
services:
postgres_test:
image: postgres:16-alpine
environment:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: testdb
ports:
- "5433:5432" # используем нестандартный порт, чтобы не мешать локальной БД
tmpfs: /var/lib/postgresql/data # данные в памяти для скорости
В Go-тесте поднимаем контейнер через os/exec или используем библиотеку testcontainers-go. Покажем оба подхода.
2.1. Ручной запуск Docker Compose из теста
// main_test.go
package main
import (
"context"
"fmt"
"os/exec"
"testing"
"time"
)
func setupTestDB(t *testing.T) string {
t.Helper()
// Запускаем контейнер
cmd := exec.Command("docker-compose", "-f", "docker-compose.test.yml", "up", "-d")
if err := cmd.Run(); err != nil {
t.Fatalf("failed to start docker-compose: %v", err)
}
// Ждём готовности PostgreSQL
time.Sleep(3 * time.Second)
return "postgres://test:test@localhost:5433/testdb?sslmode=disable"
}
func TestSaveUser(t *testing.T) {
dsn := setupTestDB(t)
db, err := sql.Open("postgres", dsn)
if err != nil {
t.Fatal(err)
}
defer db.Close()
// ... тестовый код
}
Недостатки: ручной sleep, сложность с параллельными тестами. Лучше использовать Testcontainers.
2.2. Testcontainers для Go
Установите библиотеку:
go get github.com/testcontainers/testcontainers-go
Пример с автоматическим управлением контейнером:
// repository_test.go
package repository
import (
"context"
"testing"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
"github.com/jackc/pgx/v5/pgxpool"
)
func TestUserRepository_Save(t *testing.T) {
ctx := context.Background()
// Запрашиваем PostgreSQL контейнер
req := testcontainers.ContainerRequest{
Image: "postgres:16-alpine",
ExposedPorts: []string{"5432/tcp"},
Env: map[string]string{
"POSTGRES_USER": "test",
"POSTGRES_PASSWORD": "test",
"POSTGRES_DB": "testdb",
},
WaitingFor: wait.ForLog("database system is ready to accept connections"),
}
postgres, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
if err != nil {
t.Fatal(err)
}
defer postgres.Terminate(ctx) // автоматическая очистка
// Получаем динамический порт
port, _ := postgres.MappedPort(ctx, "5432")
dsn := fmt.Sprintf("postgres://test:test@localhost:%s/testdb?sslmode=disable", port.Port())
pool, _ := pgxpool.New(ctx, dsn)
defer pool.Close()
repo := NewUserRepository(pool)
err = repo.Save(ctx, User{Name: "Alice", Email: "alice@example.com"})
if err != nil {
t.Errorf("Save() error = %v", err)
}
}
Преимущества: нет sleep, динамические порты, параллельные тесты не конфликтуют, контейнеры удаляются автоматически.
3. Интеграционное тестирование с Redis и внешними API
Часто приложение зависит от нескольких сервисов. Testcontainers легко комбинирует контейнеры.
3.1. Тестирование кэширования с Redis
func TestCacheWithRedis(t *testing.T) {
ctx := context.Background()
// Запускаем Redis
redisReq := testcontainers.ContainerRequest{
Image: "redis:7-alpine",
ExposedPorts: []string{"6379/tcp"},
WaitingFor: wait.ForLog("Ready to accept connections"),
}
redisC, _ := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: redisReq,
Started: true,
})
defer redisC.Terminate(ctx)
redisPort, _ := redisC.MappedPort(ctx, "6379")
rdb := redis.NewClient(&redis.Options{
Addr: fmt.Sprintf("localhost:%s", redisPort.Port()),
})
defer rdb.Close()
// Тестируем кэширование
err := rdb.Set(ctx, "key", "value", 0).Err()
if err != nil {
t.Fatal(err)
}
val, _ := rdb.Get(ctx, "key").Result()
if val != "value" {
t.Errorf("expected 'value', got %s", val)
}
}
3.2. Мокирование внешнего API с WireMock
Для тестирования HTTP-клиентов используем контейнер WireMock или MockServer:
func TestExternalAPIClient(t *testing.T) {
ctx := context.Background()
// Запускаем WireMock с предустановленным стабом
wiremockReq := testcontainers.ContainerRequest{
Image: "wiremock/wiremock:latest",
ExposedPorts: []string{"8080/tcp"},
Waiting