Integration и contract testing в Go
Зачем знать. Юнит-тесты не ловят интеграционные баги (drift в схеме БД, изменения API между сервисами). Middle 2 Go-инженер обязан уметь поднимать реальные зависимости (PostgreSQL, Redis, Kafka) через testcontainers, держать тесты быстрыми (изоляция через schema, reusable containers), писать contract tests на Pact для асинхронной коммуникации между микросервисами и понимать когда достаточно schema testing (protobuf).
Содержание
Заголовок раздела «Содержание»- Концепция: уровни тестирования
- Production-практики: testcontainers, suite, Pact, fixtures
- Gotchas
- Real cases
- 25 вопросов
- Practice
- Источники
1. Концепция (кратко)
Заголовок раздела «1. Концепция (кратко)»1.1 Пирамида тестов (и её критика)
Заголовок раздела «1.1 Пирамида тестов (и её критика)» /\ e2e: медленные, флаки, последняя миля / \ /----\ integration: проверяют состыковку / \ компонентов (БД, кэш, HTTP-клиент…) /--------\ / \ unit: быстрые, изолированные /____________\В микросервисной архитектуре E2E «всё со всем» нереалистично; на смену приходит contract testing: каждый сервис проверяется отдельно против контракта.
1.2 Что такое integration test в Go
Заголовок раздела «1.2 Что такое integration test в Go»- Запускается рядом с unit (
go test ./...), но с реальными зависимостями (БД, брокер, кэш). - Зависимости поднимаются в контейнерах (testcontainers-go) или как fixtures.
- Скорость: типично 10ms–1s на тест.
1.3 Что такое contract test
Заголовок раздела «1.3 Что такое contract test»- Тест проверяет, что сервис A (consumer) и сервис B (provider) ожидают одинаковую форму API/event.
- Pact: consumer-driven — consumer пишет ожидания, провайдер их верифицирует.
- Schema/Protobuf: декларативный контракт.
1.4 Карта: что тестируется чем
Заголовок раздела «1.4 Карта: что тестируется чем»| Что проверяем | Тип теста |
|---|---|
| Логика функции | unit |
| SQL-запрос, transaction | integration с реальной БД |
| HTTP-handler + routes | integration (httptest) |
| Producer записал event | integration с реальным брокером |
| Consumer обработал event | integration |
| Сервис A ↔ Сервис B по HTTP | contract (Pact) |
| Сервис A ↔ Сервис B по gRPC | schema (protobuf compatibility) |
| Сценарий: «оформить заказ» | E2E (минимально) |
2. Production-практики
Заголовок раздела «2. Production-практики»2.1 testcontainers-go: основа
Заголовок раздела «2.1 testcontainers-go: основа»
github.com/testcontainers/testcontainers-go— стандартная Go-библиотека (2024+). Использует Docker (или Podman, Rancher Desktop).
Простой пример: Postgres
Заголовок раздела «Простой пример: Postgres»import ( "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/postgres" "github.com/testcontainers/testcontainers-go/wait")
func TestMain(m *testing.M) { ctx := context.Background() pgC, err := postgres.Run(ctx, "postgres:16-alpine", postgres.WithDatabase("testdb"), postgres.WithUsername("test"), postgres.WithPassword("test"), postgres.BasicWaitStrategies(), ) if err != nil { log.Fatal(err) }
dsn, _ := pgC.ConnectionString(ctx, "sslmode=disable") db, _ := sql.Open("pgx", dsn) runMigrations(db)
code := m.Run() _ = pgC.Terminate(ctx) os.Exit(code)}Wait strategies
Заголовок раздела «Wait strategies»Контейнер «запущен» != «готов принимать запросы». Стратегии:
testcontainers.GenericContainerRequest{ ContainerRequest: testcontainers.ContainerRequest{ Image: "redis:7-alpine", ExposedPorts: []string{"6379/tcp"}, WaitingFor: wait.ForAll( wait.ForLog("Ready to accept connections"), wait.ForListeningPort("6379/tcp"), wait.ForExec([]string{"redis-cli", "ping"}). WithExitCodeMatcher(func(c int) bool { return c == 0 }), ).WithStartupTimeoutDefault(30 * time.Second), },}Доступны: wait.ForLog, wait.ForListeningPort, wait.ForHTTP, wait.ForExec, wait.ForSQL. Без явной wait — флаки!
Reusable containers
Заголовок раздела «Reusable containers»req.Reuse = truereq.Name = "my-test-pg"Если контейнер с таким именем уже есть — переиспользуется. Ускоряет dev (но не для CI).
# в окружении:export TESTCONTAINERS_REUSE_ENABLE=trueNetworks: контейнеры разговаривают
Заголовок раздела «Networks: контейнеры разговаривают»nw, _ := testcontainers.NewNetwork(ctx, network.WithDriver("bridge"))defer nw.Remove(ctx)
pgReq := testcontainers.ContainerRequest{ Image: "postgres:16", Networks: []string{nw.Name}, NetworkAliases: map[string][]string{nw.Name: {"db"}}, Env: map[string]string{"POSTGRES_PASSWORD": "test"},}
appReq := testcontainers.ContainerRequest{ Image: "myapp:dev", Networks: []string{nw.Name}, Env: map[string]string{"DATABASE_URL": "postgres://postgres:test@db:5432/postgres"},}Здесь app внутри сети обращается к Postgres по имени db.
Готовые модули
Заголовок раздела «Готовые модули»testcontainers-go/modules/*: postgres, mysql, mongodb, redis, kafka, redpanda, rabbitmq, nats, elasticsearch, vault, localstack (AWS-emulation), и десятки других. Покрывают типичные настройки + sensible wait strategies.
Custom container из Dockerfile
Заголовок раздела «Custom container из Dockerfile»req := testcontainers.ContainerRequest{ FromDockerfile: testcontainers.FromDockerfile{ Context: "./testdata", Dockerfile: "Dockerfile.test", },}2.2 Kafka / Redpanda в тестах
Заголовок раздела «2.2 Kafka / Redpanda в тестах»import "github.com/testcontainers/testcontainers-go/modules/redpanda"
c, _ := redpanda.Run(ctx, "docker.redpanda.com/redpandadata/redpanda:v24.2.7")brokers, _ := c.KafkaSeedBroker(ctx)producer, _ := kgo.NewClient(kgo.SeedBrokers(brokers))Redpanda идеально для тестов: ~3-5 секунд старта, Kafka-API совместимый.
2.3 testify/suite — группировка тестов
Заголовок раздела «2.3 testify/suite — группировка тестов»type OrderSuite struct { suite.Suite db *sql.DB repo *OrderRepo}
func (s *OrderSuite) SetupSuite() { // один раз на весь suite s.db = openTestDB() s.repo = NewOrderRepo(s.db)}
func (s *OrderSuite) SetupTest() { // перед каждым Test* _, err := s.db.Exec("TRUNCATE orders CASCADE") s.Require().NoError(err)}
func (s *OrderSuite) TearDownSuite() { s.db.Close() }
func (s *OrderSuite) TestCreate() { o, err := s.repo.Create(ctx, Order{Total: 100}) s.NoError(err) s.NotZero(o.ID)}
func TestOrderSuite(t *testing.T) { suite.Run(t, new(OrderSuite))}Минусы: parallelism сложнее (suite-level state шарится), но для интеграционных тестов часто оптимально.
2.4 Изоляция тестов: транзакция, schema, drop
Заголовок раздела «2.4 Изоляция тестов: транзакция, schema, drop»Три стратегии:
1) Транзакция-rollback (быстро):
func (s *Suite) SetupTest() { tx, _ := s.db.BeginTx(ctx, nil) s.tx = tx}func (s *Suite) TearDownTest() { s.tx.Rollback()}⚠️ Не работает если код сам управляет транзакциями.
2) TRUNCATE между тестами:
_, _ = s.db.Exec("TRUNCATE orders, customers, payments RESTART IDENTITY CASCADE")Среднее по скорости; учитывает FK.
3) Schema-per-test (Postgres):
schema := "test_" + uuid.NewString()[:8]s.db.Exec("CREATE SCHEMA " + schema)s.db.Exec("SET search_path TO " + schema)// ... run migrations ...// в конце:s.db.Exec("DROP SCHEMA " + schema + " CASCADE")Полная изоляция, можно гонять параллельно; стоимость — миграции каждый раз.
Гибрид (рекомендуется): schema per-suite + TRUNCATE per-test.
2.5 Fixtures и factories
Заголовок раздела «2.5 Fixtures и factories»// Builder patterntype OrderBuilder struct{ o Order }func NewOrder() *OrderBuilder { return &OrderBuilder{o: Order{Total: 100, Status: "new"}} }func (b *OrderBuilder) WithTotal(t int) *OrderBuilder { b.o.Total = t; return b }func (b *OrderBuilder) Build() Order { return b.o }Полезно: testdata/*.json для готовых fixture; goldenfiles для сравнения output:
got := render(input)golden := filepath.Join("testdata", t.Name()+".golden")if *update { os.WriteFile(golden, got, 0644) }want, _ := os.ReadFile(golden)require.Equal(t, want, got)go test -update — обновляет golden-файлы (флаг кастомный).
2.6 Contract testing с Pact
Заголовок раздела «2.6 Contract testing с Pact»Микросервисы интегрируются по HTTP/gRPC/events. E2E «всё со всем» — медленно и хрупко. Pact:
Consumer пишет тест ──▶ pact-файл (JSON-ожидания) │ ▼ Pact broker │ ▼ Provider в CI ──▶ verify(pact) ──▶ pass/failConsumer-сторона (Go, pact-go v2)
Заголовок раздела «Consumer-сторона (Go, pact-go v2)»import "github.com/pact-foundation/pact-go/v2/consumer"
mockProvider, _ := consumer.NewV4Pact(consumer.MockHTTPProviderConfig{ Consumer: "orders-service", Provider: "billing-service",})
err := mockProvider. AddInteraction(). Given("user 42 exists"). UponReceiving("request for balance of user 42"). WithRequest(http.MethodGet, "/users/42/balance", func(b *consumer.V4InteractionWithRequestBuilder) { b.Header("Accept", matchers.S("application/json")) }). WillRespondWith(200, func(b *consumer.V4InteractionWithResponseBuilder) { b.Header("Content-Type", matchers.S("application/json")) b.JSONBody(matchers.Map{ "user_id": matchers.Like(42), "balance": matchers.Decimal(150.25), "currency": matchers.S("USD"), }) }). ExecuteTest(t, func(cfg consumer.MockServerConfig) error { client := billing.New(fmt.Sprintf("http://%s:%d", cfg.Host, cfg.Port)) b, err := client.GetBalance(42) require.NoError(t, err) require.Equal(t, 150.25, b.Balance) return nil })
require.NoError(t, err)// в результате: pacts/orders-service-billing-service.jsonPact Broker
Заголовок раздела «Pact Broker»Self-hosted (pactfoundation/pact-broker) или managed (pactflow.io). Consumer публикует pact:
pact-broker publish ./pacts \ --consumer-app-version=$GIT_SHA \ --broker-base-url=https://broker.example.comProvider-сторона (verify)
Заголовок раздела «Provider-сторона (verify)»import "github.com/pact-foundation/pact-go/v2/provider"
func TestProvider(t *testing.T) { server := startBillingServer() // на random порту defer server.Close()
verifier := provider.NewVerifier() err := verifier.VerifyProvider(t, provider.VerifyRequest{ Provider: "billing-service", ProviderBaseURL: server.URL, BrokerURL: "https://broker.example.com", PublishVerificationResults: true, ProviderVersion: os.Getenv("GIT_SHA"), StateHandlers: provider.StateHandlers{ "user 42 exists": func(setup bool, state provider.ProviderState) (provider.ProviderStateResponse, error) { // подготовить БД: вставить user 42 с балансом 150.25 return nil, seedUserBalance(42, 150.25) }, }, }) require.NoError(t, err)}Версионирование и can-i-deploy
Заголовок раздела «Версионирование и can-i-deploy»pact-broker can-i-deploy --pacticipant=orders-service \ --version=$GIT_SHA --to-environment=productionВозвращает «можно ли мержить» — если consumer не проверен против актуальной версии provider в проде, deploy блокируется.
Pact для events / messaging
Заголовок раздела «Pact для events / messaging»Pact поддерживает message pacts для асинхронной коммуникации (Kafka, NATS, RabbitMQ): consumer описывает форму ожидаемого сообщения, provider тестируется на корректность producer.
2.7 Schema testing vs Contract testing
Заголовок раздела «2.7 Schema testing vs Contract testing»- Schema — структурная проверка (protobuf, OpenAPI, JSON Schema). Достаточно если семантика контракта = форма данных.
- Contract — структурная + behavioral (определённый request → определённый response).
Для gRPC: protobuf-схема + buf breaking (https://buf.build/docs/breaking-overview) часто достаточны.
2.8 Альтернативы Pact
Заголовок раздела «2.8 Альтернативы Pact»- Spring Cloud Contract (Java-ориентирован).
- OpenAPI + Dredd — валидация против спецификации.
- WireMock — record/replay HTTP, локально для test isolation.
2.9 docker-compose vs testcontainers
Заголовок раздела «2.9 docker-compose vs testcontainers»- docker-compose — стабильное окружение для команды, медленный startup (минуты), сложно изолировать тесты.
- testcontainers — per-test/per-suite контейнеры, изоляция, чуть выше стартовое время на тест.
В CI: используй testcontainers — гарантия чистоты. Локально: иногда удобнее долго работающий compose.
2.10 CI: docker-in-docker
Заголовок раздела «2.10 CI: docker-in-docker»В GitHub Actions / GitLab CI testcontainers требует Docker socket внутри runner. Варианты:
- Privileged DinD — медленно и небезопасно.
- Docker socket mount (
/var/run/docker.sock) — рекомендация. - Testcontainers Cloud — managed runtime (Atomicjar / Docker Inc).
# GitHub Actions- uses: actions/checkout@v4- uses: actions/setup-go@v5- run: go test ./... -tags=integration env: TESTCONTAINERS_RYUK_DISABLED: false # default true для CIRyuk — companion container для cleanup; в Kubernetes может потребовать отключения.
2.11 Тэги для разделения unit/integration
Заголовок раздела «2.11 Тэги для разделения unit/integration»//go:build integrationpackage orders_testЗапуск:
go test ./... # только unitgo test -tags=integration ./... # + integration3. Gotchas
Заголовок раздела «3. Gotchas»⚠️ Forget Terminate/Cleanup. Контейнеры остаются «висеть»; Ryuk их убирает, но в CI без Ryuk будут утечки. Используй t.Cleanup(...).
⚠️ Запуск контейнера ≠ готов. Wait strategy обязательна (ForLog, ForListeningPort, ForSQL).
⚠️ testcontainers стартует медленно на M1/M2 — для arm64 image нужны мульти-arch (linux/amd64,linux/arm64). Иначе эмуляция через Rosetta = 5x медленнее.
⚠️ Reusable + parallel tests = race. Если суиты переиспользуют контейнер и пишут в одну БД — данные пересекаются. Изолируй через schema.
⚠️ Database miss state между миграциями. Если миграции изменены — drop schema целиком, иначе остаточные таблицы.
⚠️ Pact: «like» matchers слишком permissive. matchers.Like(42) matches любому integer — лучше matchers.Integer(42) если важно тип.
⚠️ Pact «state handlers» — общее состояние provider. State handler меняет реальную БД; гонки если параллельно. Изолируй (schema/transaction).
⚠️ Pact версионирование. Pact-файл версионируется по версии consumer; broker хранит совместимость. Если pact_file_path не уникален — затирается.
⚠️ Goldenfiles забывают обновлять. После рефакторинга тест падает «по делу», но кажется как «тест сломался». Документируй процесс update.
⚠️ TRUNCATE без CASCADE при FK → ошибка. Всегда CASCADE + RESTART IDENTITY.
⚠️ Контейнер на random порту. Никогда не hardcode 5432 — бери Container.MappedPort(ctx, "5432/tcp").
⚠️ Тестовая БД с production-credentials — фактический инцидент в команде, отрабатывало на проде. Изолируй DSN, не используй env vars из общего пула.
⚠️ t.Parallel() + shared container = ID коллизии. Либо отдельная schema/db на тест, либо отказ от t.Parallel() для интеграционных.
⚠️ CI cache контейнерных image — без явного кеша pull каждый раз → медленно. Используй docker pull step или tc.WithImagePlatform.
⚠️ Ryuk не работает в Kubernetes-runner (нужен Docker socket). Отключи + ручной cleanup в TestMain.
4. Real cases
Заголовок раздела «4. Real cases»4.1 Integration suite на средний сервис
Заголовок раздела «4.1 Integration suite на средний сервис»Сервис orders (Go):
- 80 unit-тестов: business logic.
- 30 integration-тестов: testcontainers (PG + Redpanda + Redis).
- 5 contract-тестов: Pact (consumer для
billing,inventory). - 1 «happy path» E2E.
Время прогона: 90 секунд (parallel suites).
4.2 Pact в monorepo
Заголовок раздела «4.2 Pact в monorepo»20 микросервисов; Pact broker деплоится как часть платформы; can-i-deploy встроен в release пайплайн → нельзя задеплоить consumer, если его pact не верифицирован против актуального provider в production.
4.3 Redpanda для тестов вместо Kafka
Заголовок раздела «4.3 Redpanda для тестов вместо Kafka»Команда меняет confluentinc/cp-kafka (45 секунд startup + 2 GB RAM) на redpandadata/redpanda (3 секунды + 200 MB). Тесты быстрее в ~10×, локальная разработка приятнее.
4.4 Schema testing для gRPC
Заголовок раздела «4.4 Schema testing для gRPC»Вместо полного Pact для всех gRPC-вызовов команда настроила buf breaking в CI: любой breaking change в .proto блокирует мердж. Pact оставлен только для критических REST API.
5. Вопросы
Заголовок раздела «5. Вопросы»1. Что такое integration-тест? Тест с реальной зависимостью (БД, брокер, кэш) — поднимает локальный экземпляр (контейнер), запускает код против неё.
2. Что такое testcontainers-go? Библиотека для Go, поднимающая Docker-контейнеры для тестов; есть готовые модули (postgres, redis, kafka и т.д.).
3. Зачем wait strategies?
Контейнер «started» != «ready». Без явного ожидания (ForLog, ForListeningPort, ForSQL) тесты флакают.
4. Что такое reusable container? Если контейнер с заданным именем уже запущен — testcontainers переиспользует. Ускоряет dev-цикл; в CI обычно выключают.
5. Как контейнеры обмениваются между собой?
Через явную Docker network (testcontainers.NewNetwork) + alias-имя; внутри сети обращаются по имени.
6. Как изолировать тесты на одной БД? Schema-per-test + DROP CASCADE; либо TRUNCATE+RESTART IDENTITY; либо транзакция-rollback (если код не своих транзакций не делает).
7. Что такое testify/suite?
Хелпер для группировки тестов с SetupSuite/SetupTest/TearDownTest/TearDownSuite.
8. Что такое goldenfiles? Файлы-эталоны с ожидаемым output; тест сравнивает текущий вывод с golden. Для большого структурированного output (JSON, SQL plan).
9. Что такое contract testing? Проверка соответствия между двумя сервисами по контракту (форма + behavior). Заменяет полный E2E.
10. Что такое Pact? Фреймворк consumer-driven contract testing: consumer пишет ожидания, publish в broker, provider verify.
11. Чем Pact отличается от schema-теста? Schema — только структура. Pact — структура + сценарии (Given/When/Then) + verify provider.
12. Что такое Pact broker?
Сервис хранит pact-файлы между версиями; интегрируется с can-i-deploy для блокировки deploy если контракт не верифицирован.
13. Что такое can-i-deploy?
CLI-команда Pact: проверяет, что версия consumer верифицирована против актуальной версии provider в targeted environment.
14. Что такое state handlers в Pact? Хуки на стороне provider: «при ожидании provider state X — приведи систему в состояние X (вставь данные)».
15. Когда достаточно schema testing вместо Pact? Когда контракт исчерпывается формой данных: protobuf для gRPC, OpenAPI с строгим валидатором. Pact добавляет ценность для behavioral-сценариев.
16. Чем отличается docker-compose от testcontainers? Compose — статичное долгоживущее окружение. Testcontainers — per-test, изолированное, программируемое из кода.
17. Что такое Ryuk? Companion-контейнер testcontainers, который автоматически удаляет тестовые контейнеры после завершения. В Kubernetes/некоторых CI может быть отключён.
18. Как тэгировать integration-тесты?
//go:build integration + запуск go test -tags=integration.
19. Что выбрать для Kafka в тестах — реальный Kafka или Redpanda? Redpanda — быстрее старт, легче, Kafka API совместим. Если нужны специфичные Kafka-фичи (Streams) — реальный Kafka.
20. Как тестировать producer ↔ consumer одного приложения? Поднимаем брокер (testcontainers), publish + assert что consumer получил/обработал (через канал в тесте или через side-effect в БД).
21. Как симулировать сбои зависимостей в integration? toxiproxy (FT/latency/disconnect между app и зависимостью), pause/unpause контейнера, или explicitly Stop()/Start() container в тесте.
22. Что такое provider state в Pact? Метка состояния, которое provider должен подготовить перед сценарием (например «user 42 exists»). Handler делает setup.
23. Что такое message pact? Pact-расширение для асинхронной коммуникации (Kafka/NATS/RabbitMQ): тестируется форма сообщения и логика consumer.
24. Что такое buf breaking? CLI-инструмент Buf, выявляющий breaking changes в protobuf-схемах. Часто заменяет contract test для gRPC.
25. Как ускорить integration suite?
- shared контейнер с per-test schema;
- parallel test ((
t.Parallel)); - Redpanda вместо Kafka;
- кеш Docker image в CI;
- migrations один раз на suite, TRUNCATE между тестами.
6. Practice
Заголовок раздела «6. Practice»-
PG + миграции. Поднять
postgres:16-alpineчерез testcontainers, прогнать миграции (golang-migrate), реализовать тестRepository.Create + Find. -
TRUNCATE-isolation. Реализуй
SetupTest/TearDownTestдля testify/suite через TRUNCATE; запусти 50 тестов, измерь время. -
Schema-per-test. Заверни тесты в
CREATE SCHEMA test_<random> + SET search_path + DROP SCHEMA CASCADE; сравни параллелизм vs TRUNCATE. -
Kafka producer/consumer integration. Подними Redpanda, проверь что producer публикует event, consumer его получает в течение N миллисекунд.
-
Networks: app + DB. Запусти 2 контейнера в одной сети (app + Postgres); из теста дернуть HTTP /health в app, который ходит в Postgres.
-
toxiproxy. Положи toxiproxy между app и Postgres, симулируй 500ms latency, проверь как app ведёт себя (timeout, retry).
-
Pact consumer-side. Напиши consumer-тест для
billing-service(Pact mock); publish pact-файл; verify локально provider-сторону. -
Pact broker. Подними
pactfoundation/pact-brokerв docker-compose; publish pact из CI, verify на provider; реализуйcan-i-deploy. -
Goldenfiles. Реализуй goldenfile-тест для рендеринга JSON ответа; добавь флаг
-updateдля обновления golden. -
buf breaking. Создай proto-схему сервиса, измени поле (rename), запусти
buf breaking— должно остановить.
7. Дополнительные блоки
Заголовок раздела «7. Дополнительные блоки»7.1 Полный пример: integration suite с PG + Kafka
Заголовок раздела «7.1 Полный пример: integration suite с PG + Kafka»//go:build integrationpackage orders_test
type IntegrationSuite struct { suite.Suite pg *postgres.PostgresContainer rp *redpanda.Container db *pgxpool.Pool kafka *kgo.Client repo *OrderRepo pub *OrderPublisher}
func (s *IntegrationSuite) SetupSuite() { ctx := context.Background() var err error
s.pg, err = postgres.Run(ctx, "postgres:16-alpine", postgres.WithDatabase("orders"), postgres.WithUsername("test"), postgres.WithPassword("test"), postgres.BasicWaitStrategies()) s.Require().NoError(err)
s.rp, err = redpanda.Run(ctx, "docker.redpanda.com/redpandadata/redpanda:v24.2.7") s.Require().NoError(err)
dsn, _ := s.pg.ConnectionString(ctx, "sslmode=disable") s.db, _ = pgxpool.New(ctx, dsn) s.Require().NoError(runMigrations(s.db))
brokers, _ := s.rp.KafkaSeedBroker(ctx) s.kafka, _ = kgo.NewClient(kgo.SeedBrokers(brokers))
s.repo = NewOrderRepo(s.db) s.pub = NewOrderPublisher(s.kafka, "orders")}
func (s *IntegrationSuite) SetupTest() { _, err := s.db.Exec(context.Background(), "TRUNCATE orders RESTART IDENTITY CASCADE") s.Require().NoError(err)}
func (s *IntegrationSuite) TearDownSuite() { s.db.Close() s.kafka.Close() _ = s.rp.Terminate(context.Background()) _ = s.pg.Terminate(context.Background())}
func (s *IntegrationSuite) TestCreateOrder_PublishesEvent() { ctx := context.Background()
o, err := s.repo.Create(ctx, NewOrder().WithTotal(500).Build()) s.Require().NoError(err) s.Require().NoError(s.pub.OrderCreated(ctx, o))
consumer, _ := kgo.NewClient( kgo.SeedBrokers(s.brokers()), kgo.ConsumeTopics("orders"), kgo.ConsumerGroup("test-"+uuid.NewString()), ) defer consumer.Close()
fetchCtx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() fetches := consumer.PollFetches(fetchCtx) s.Require().NoError(fetches.Err0())
var got *kgo.Record fetches.EachRecord(func(r *kgo.Record) { got = r }) s.Require().NotNil(got) s.Equal(o.ID, string(got.Key))}
func TestIntegrationSuite(t *testing.T) { if testing.Short() { t.Skip("integration tests") } suite.Run(t, new(IntegrationSuite))}7.2 Migration в testcontainers
Заголовок раздела «7.2 Migration в testcontainers»Запускать миграции один раз при SetupSuite. golang-migrate:
import ( "github.com/golang-migrate/migrate/v4" _ "github.com/golang-migrate/migrate/v4/database/postgres" _ "github.com/golang-migrate/migrate/v4/source/file")
func runMigrations(db *pgxpool.Pool) error { m, err := migrate.New("file://./migrations", dsnFromPool(db)) if err != nil { return err } if err := m.Up(); err != nil && err != migrate.ErrNoChange { return err } return nil}⚠️ Не пересоздавай контейнер на каждый тест — миграции медленные.
7.3 Параллельные интеграционные тесты
Заголовок раздела «7.3 Параллельные интеграционные тесты»func TestParallel(t *testing.T) { schema := setupSchema(t) t.Cleanup(func() { dropSchema(schema) }) t.Parallel() // ...}
func setupSchema(t *testing.T) string { t.Helper() schema := "test_" + strings.ReplaceAll(uuid.NewString()[:8], "-", "") _, err := globalDB.Exec(ctx, fmt.Sprintf(`CREATE SCHEMA "%s"`, schema)) require.NoError(t, err) return schema}Каждый параллельный тест получает свою схему. Главное — никаких глобальных таблиц/секвенций без quoting.
7.4 Запись Pact-файла + provider verify (полный pipeline)
Заголовок раздела «7.4 Запись Pact-файла + provider verify (полный pipeline)»consumer-side:
go test ./pact/consumer/...# pacts/orders-service-billing-service.json создан
# publishpact-broker publish ./pacts \ --consumer-app-version=$(git rev-parse HEAD) \ --tag=master \ --broker-base-url=https://broker.example.comprovider-side (в CI billing-service):
go test ./pact/provider/... \ -broker-url=https://broker.example.com \ -provider-version=$(git rev-parse HEAD)deploy gate:
pact-broker can-i-deploy \ --pacticipant=billing-service \ --version=$(git rev-parse HEAD) \ --to-environment=production# exit 0 если все consumers совместимы, иначе fail7.5 Pact: типичные matcher’ы
Заголовок раздела «7.5 Pact: типичные matcher’ы»matchers.Like(42) // любой integer 32 bitmatchers.Integer(42) // именно intmatchers.Decimal(1.23) // именно float/decimalmatchers.S("USD") // exact string "USD"matchers.Regex("ABC123", `^[A-Z]+\d+$`)matchers.UUID() // любой UUID v4matchers.Date("2026-05-21")matchers.Timestamp("yyyy-MM-dd'T'HH:mm:ssZ", "2026-05-21T12:00:00+0000")matchers.EachLike(matchers.Like(0), 3) // array из 3+ элементовmatchers.Map{ "id": matchers.UUID(), "qty": matchers.IntegerGreaterThan(0),}Перебор matcher’ов:
Like— структура должна соответствовать (по типу полей).EachLike— массив, элементы как заданный template.Term— regex.EachKeyLike— ключи в map matching pattern.
7.6 Contract tests для messaging
Заголовок раздела «7.6 Contract tests для messaging»Pact V4 поддерживает MessageInteraction: описание сообщений (Kafka, RabbitMQ).
mp, _ := message.NewMessagePactV4(message.Config{ Consumer: "billing", Provider: "orders-emitter",})
mp.AddAsynchronousMessage(). Given("order created"). ExpectsToReceive("OrderCreated event"). WithMetadata(map[string]string{"topic": "orders"}). WithJSONContent(matchers.Map{ "order_id": matchers.UUID(), "amount": matchers.Decimal(100.0), }). AsType(&OrderCreatedEvent{}). ConsumedBy(func(event message.AsynchronousMessage) error { // вызываем настоящий handler var ev OrderCreatedEvent json.Unmarshal(event.Content, &ev) return billingHandler.Handle(ev) }). Verify(t)Provider в CI должен показать что producer выдаёт строго такое же сообщение.
7.7 Goldenfiles + JSON-нормализация
Заголовок раздела «7.7 Goldenfiles + JSON-нормализация»Сравнивать сырые байты JSON хрупко (порядок полей). Нормализуй:
func normalize(t *testing.T, raw []byte) []byte { var v any require.NoError(t, json.Unmarshal(raw, &v)) out, err := json.MarshalIndent(sortMaps(v), "", " ") require.NoError(t, err) return out}
func TestRenderOrder(t *testing.T) { got := normalize(t, renderOrder(testOrder)) golden := filepath.Join("testdata", "order.golden.json") if *update { os.WriteFile(golden, got, 0644); return } want, _ := os.ReadFile(golden) require.Equal(t, string(want), string(got))}7.8 Кейс: testcontainers для Redis cluster
Заголовок раздела «7.8 Кейс: testcontainers для Redis cluster»import "github.com/testcontainers/testcontainers-go/modules/redis"
c, err := redis.Run(ctx, "redis:7-alpine", redis.WithSnapshotting(10, 1))addr, _ := c.ConnectionString(ctx)client := goredis.NewClient(&goredis.Options{Addr: stripScheme(addr)})Для cluster — нужен custom container через GenericContainer с конфигурацией redis-cli --cluster create.
7.9 Performance optimization integration suite
Заголовок раздела «7.9 Performance optimization integration suite»| Приём | Время прогона до | После | Эффект |
|---|---|---|---|
| Shared container на suite | 120s | 60s | контейнер не пересоздается |
| TRUNCATE вместо drop schema | 60s | 35s | миграции 1 раз |
| Parallel (per-schema) | 35s | 20s | t.Parallel() |
| Redpanda вместо Kafka | 20s | 10s | старт быстрее |
| Cache Docker images | 10s | 8s | docker pull в CI cache |
7.10 Чек-лист integration testing
Заголовок раздела «7.10 Чек-лист integration testing»- testcontainers + готовые modules где есть.
- Wait strategies на каждый контейнер.
- testify/suite для группировки.
- Изоляция: schema-per-suite + TRUNCATE per-test.
- Миграции один раз, не пересоздавать БД.
-
//go:build integration— отделить от unit. -
t.CleanupилиTearDownSuiteдля контейнеров. - CI настройки: Docker socket, image cache.
- DSN через port mapping, не hardcode.
- Параллелизм только если изоляция гарантирована.
7.11 Чек-лист contract testing
Заголовок раздела «7.11 Чек-лист contract testing»- Pact для критичных HTTP/REST между сервисами.
- Schema testing (buf breaking) для gRPC.
- Consumer-driven contracts (не provider-driven).
- Pact broker +
can-i-deployв release pipeline. - State handlers изолированы (schema/transaction).
- Matchers явные (
Integer, неLikeгде возможно). - Pact версионируется по git SHA.
- Tag
master/productionдля деплоев. - Message pacts для асинхронной коммуникации.
8. Источники
Заголовок раздела «8. Источники»- testcontainers-go documentation — официально.
- testcontainers modules — список готовых модулей.
- Pact: Consumer-driven contract testing — официальная документация.
- pact-foundation/pact-go v2 — Go-клиент.
- Buf: breaking change detection — protobuf compatibility.
- testify/suite — suite helpers.
- Redpanda for testing — Kafka-compatible альтернатива.
- toxiproxy — chaos между app и зависимостями.
- «Testing Microservices» — Cindy Sridharan — про контрактное тестирование.
- «Goldenfiles» pattern (Mitchell Hashimoto) — про сравнение output.