Интеграционное тестирование Go с Docker: полное руководство
Интеграционное тестирование — критически важный этап разработки, особенно в мире микросервисов и распределенных систем. Go (Golang) в сочетании с Docker предоставляет мощные и элегантные инструменты для создания надежных интеграционных тестов. В этой статье мы разберем, как настроить окружение, написать тесты для взаимодействия с базами данных и внешними сервисами, используя Docker, testcontainers-go и стандартную библиотеку testing.
1. Почему Docker для интеграционного тестирования?
Традиционные модульные тесты (unit tests) изолируют логику, но не проверяют взаимодействие с реальными зависимостями: базами данных, очередями, API. Docker решает эту проблему, позволяя:
- Изолировать окружение: Каждый тест запускает чистый контейнер, исключая конфликты состояний.
- Повторяемость: Одинаковое окружение на локальной машине и в CI/CD.
- Быстрое развертывание: Контейнеры запускаются за секунды.
- Простота очистки: После теста контейнер уничтожается.
Мы будем использовать библиотеку testcontainers-go — она предоставляет высокоуровневый API для управления жизненным циклом контейнеров прямо из кода тестов.
2. Настройка проекта и установка зависимостей
Создадим простой HTTP-сервер на Go, который сохраняет данные в PostgreSQL. Затем напишем интеграционный тест, проверяющий эндпоинт.
Структура проекта:
myapp/
├── main.go
├── go.mod
├── go.sum
├── handlers/
│ └── user.go
└── integration_test.go
Шаг 1: Инициализируем модуль и установим testcontainers:
go mod init myapp
go get github.com/testcontainers/testcontainers-go
go get github.com/testcontainers/testcontainers-go/modules/postgres
go get github.com/lib/pq
go get github.com/stretchr/testify/assert
Шаг 2: Напишем простой обработчик в handlers/user.go:
package handlers
import (
"database/sql"
"encoding/json"
"net/http"
)
type User struct {
Name string `json:"name"`
Age int `json:"age"`
}
func CreateUser(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var user User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
_, err := db.Exec("INSERT INTO users (name, age) VALUES ($1, $2)", user.Name, user.Age)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(user)
}
}
3. Пишем интеграционный тест с Docker
Теперь самое интересное — тест, который поднимает PostgreSQL в Docker, создает таблицу, запускает наш сервер и проверяет его работу.
Файл integration_test.go:
package myapp
import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"myapp/handlers"
"github.com/lib/pq"
"github.com/stretchr/testify/assert"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/modules/postgres"
"github.com/testcontainers/testcontainers-go/wait"
)
func TestCreateUserIntegration(t *testing.T) {
ctx := context.Background()
// 1. Запускаем контейнер PostgreSQL
pgContainer, err := postgres.RunContainer(ctx,
testcontainers.WithImage("postgres:15-alpine"),
postgres.WithDatabase("testdb"),
postgres.WithUsername("test"),
postgres.WithPassword("test"),
testcontainers.WithWaitStrategy(
wait.ForLog("database system is ready to accept connections").
WithOccurrence(2).
WithStartupTimeout(60),
),
)
assert.NoError(t, err)
defer pgContainer.Terminate(ctx)
// 2. Получаем строку подключения
connStr, err := pgContainer.ConnectionString(ctx, "sslmode=disable")
assert.NoError(t, err)
// 3. Подключаемся к БД
db, err := sql.Open("postgres", connStr)
assert.NoError(t, err)
defer db.Close()
// 4. Создаем таблицу
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
age INTEGER NOT NULL
)`)
assert.NoError(t, err)
// 5. Настраиваем HTTP-сервер
handler := handlers.CreateUser(db)
server := httptest.NewServer(handler)
defer server.Close()
// 6. Отправляем тестовый запрос
user := handlers.User{Name: "Alice", Age: 30}
body, _ := json.Marshal(user)
resp, err := http.Post(server.URL+"/users", "application/json", bytes.NewReader(body))
assert.NoError(t, err)
assert.Equal(t, http.StatusCreated, resp.StatusCode)
// 7. Проверяем, что данные сохранились
var result handlers.User
json.NewDecoder(resp.Body).Decode(&result)
assert.Equal(t, "Alice", result.Name)
assert.Equal(t, 30, result.Age)
// 8. Проверяем в БД напрямую
var dbName string
err = db.QueryRow("SELECT name FROM users WHERE name = $1", "Alice").Scan(&dbName)
assert.NoError(t, err)
assert.Equal(t, "Alice", dbName)
}
Разбор ключевых моментов:
postgres.RunContainer— автоматически скачивает образ (если его нет) и запускает контейнер.wait.ForLog— ожидает готовности БД, анализируя логи контейнера.httptest.NewServer— создает локальный HTTP-сервер для тестирования без открытия портов.- Контейнер уничтожается автоматически через
defer.
4. Продвинутые техники: Testcontainers и Docker Compose
Для сложных сценариев (несколько сервисов, очереди, Redis) используйте docker-compose. Testcontainers умеет работать с ним напрямую:
import "github.com/testcontainers/testcontainers-go/compose"
func TestWithDockerCompose(t *testing.T) {
compose, err := compose.NewDockerCompose("docker-compose.yml")