Перейти к содержимому

Integration и contract testing в Go

Зачем знать. Юнит-тесты не ловят интеграционные баги (drift в схеме БД, изменения API между сервисами). Middle 2 Go-инженер обязан уметь поднимать реальные зависимости (PostgreSQL, Redis, Kafka) через testcontainers, держать тесты быстрыми (изоляция через schema, reusable containers), писать contract tests на Pact для асинхронной коммуникации между микросервисами и понимать когда достаточно schema testing (protobuf).

  1. Концепция: уровни тестирования
  2. Production-практики: testcontainers, suite, Pact, fixtures
  3. Gotchas
  4. Real cases
  5. 25 вопросов
  6. Practice
  7. Источники

/\ e2e: медленные, флаки, последняя миля
/ \
/----\ integration: проверяют состыковку
/ \ компонентов (БД, кэш, HTTP-клиент…)
/--------\
/ \ unit: быстрые, изолированные
/____________\

В микросервисной архитектуре E2E «всё со всем» нереалистично; на смену приходит contract testing: каждый сервис проверяется отдельно против контракта.

  • Запускается рядом с unit (go test ./...), но с реальными зависимостями (БД, брокер, кэш).
  • Зависимости поднимаются в контейнерах (testcontainers-go) или как fixtures.
  • Скорость: типично 10ms–1s на тест.
  • Тест проверяет, что сервис A (consumer) и сервис B (provider) ожидают одинаковую форму API/event.
  • Pact: consumer-driven — consumer пишет ожидания, провайдер их верифицирует.
  • Schema/Protobuf: декларативный контракт.
Что проверяемТип теста
Логика функцииunit
SQL-запрос, transactionintegration с реальной БД
HTTP-handler + routesintegration (httptest)
Producer записал eventintegration с реальным брокером
Consumer обработал eventintegration
Сервис A ↔ Сервис B по HTTPcontract (Pact)
Сервис A ↔ Сервис B по gRPCschema (protobuf compatibility)
Сценарий: «оформить заказ»E2E (минимально)

github.com/testcontainers/testcontainers-go — стандартная Go-библиотека (2024+). Использует Docker (или Podman, Rancher Desktop).

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)
}

Контейнер «запущен» != «готов принимать запросы». Стратегии:

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 — флаки!

req.Reuse = true
req.Name = "my-test-pg"

Если контейнер с таким именем уже есть — переиспользуется. Ускоряет dev (но не для CI).

Окно терминала
# в окружении:
export TESTCONTAINERS_REUSE_ENABLE=true
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.

req := testcontainers.ContainerRequest{
FromDockerfile: testcontainers.FromDockerfile{
Context: "./testdata",
Dockerfile: "Dockerfile.test",
},
}
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 совместимый.

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 шарится), но для интеграционных тестов часто оптимально.

Три стратегии:

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.

// Builder pattern
type 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-файлы (флаг кастомный).

Микросервисы интегрируются по HTTP/gRPC/events. E2E «всё со всем» — медленно и хрупко. Pact:

Consumer пишет тест ──▶ pact-файл (JSON-ожидания)
Pact broker
Provider в CI ──▶ verify(pact) ──▶ pass/fail
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.json

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.com
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)
}
Окно терминала
pact-broker can-i-deploy --pacticipant=orders-service \
--version=$GIT_SHA --to-environment=production

Возвращает «можно ли мержить» — если consumer не проверен против актуальной версии provider в проде, deploy блокируется.

Pact поддерживает message pacts для асинхронной коммуникации (Kafka, NATS, RabbitMQ): consumer описывает форму ожидаемого сообщения, provider тестируется на корректность producer.

  • Schema — структурная проверка (protobuf, OpenAPI, JSON Schema). Достаточно если семантика контракта = форма данных.
  • Contract — структурная + behavioral (определённый request → определённый response).

Для gRPC: protobuf-схема + buf breaking (https://buf.build/docs/breaking-overview) часто достаточны.

  • Spring Cloud Contract (Java-ориентирован).
  • OpenAPI + Dredd — валидация против спецификации.
  • WireMock — record/replay HTTP, локально для test isolation.
  • docker-compose — стабильное окружение для команды, медленный startup (минуты), сложно изолировать тесты.
  • testcontainers — per-test/per-suite контейнеры, изоляция, чуть выше стартовое время на тест.

В CI: используй testcontainers — гарантия чистоты. Локально: иногда удобнее долго работающий compose.

В 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 для CI

Ryuk — companion container для cleanup; в Kubernetes может потребовать отключения.

//go:build integration
package orders_test

Запуск:

Окно терминала
go test ./... # только unit
go test -tags=integration ./... # + integration

⚠️ 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.


Сервис 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).

20 микросервисов; Pact broker деплоится как часть платформы; can-i-deploy встроен в release пайплайн → нельзя задеплоить consumer, если его pact не верифицирован против актуального provider в production.

Команда меняет confluentinc/cp-kafka (45 секунд startup + 2 GB RAM) на redpandadata/redpanda (3 секунды + 200 MB). Тесты быстрее в ~10×, локальная разработка приятнее.

Вместо полного Pact для всех gRPC-вызовов команда настроила buf breaking в CI: любой breaking change в .proto блокирует мердж. Pact оставлен только для критических REST API.


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 между тестами.

  1. PG + миграции. Поднять postgres:16-alpine через testcontainers, прогнать миграции (golang-migrate), реализовать тест Repository.Create + Find.

  2. TRUNCATE-isolation. Реализуй SetupTest/TearDownTest для testify/suite через TRUNCATE; запусти 50 тестов, измерь время.

  3. Schema-per-test. Заверни тесты в CREATE SCHEMA test_<random> + SET search_path + DROP SCHEMA CASCADE; сравни параллелизм vs TRUNCATE.

  4. Kafka producer/consumer integration. Подними Redpanda, проверь что producer публикует event, consumer его получает в течение N миллисекунд.

  5. Networks: app + DB. Запусти 2 контейнера в одной сети (app + Postgres); из теста дернуть HTTP /health в app, который ходит в Postgres.

  6. toxiproxy. Положи toxiproxy между app и Postgres, симулируй 500ms latency, проверь как app ведёт себя (timeout, retry).

  7. Pact consumer-side. Напиши consumer-тест для billing-service (Pact mock); publish pact-файл; verify локально provider-сторону.

  8. Pact broker. Подними pactfoundation/pact-broker в docker-compose; publish pact из CI, verify на provider; реализуй can-i-deploy.

  9. Goldenfiles. Реализуй goldenfile-тест для рендеринга JSON ответа; добавь флаг -update для обновления golden.

  10. buf breaking. Создай proto-схему сервиса, измени поле (rename), запусти buf breaking — должно остановить.


//go:build integration
package 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))
}

Запускать миграции один раз при 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
}

⚠️ Не пересоздавай контейнер на каждый тест — миграции медленные.

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.

consumer-side:

Окно терминала
go test ./pact/consumer/...
# pacts/orders-service-billing-service.json создан
# publish
pact-broker publish ./pacts \
--consumer-app-version=$(git rev-parse HEAD) \
--tag=master \
--broker-base-url=https://broker.example.com

provider-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 совместимы, иначе fail
matchers.Like(42) // любой integer 32 bit
matchers.Integer(42) // именно int
matchers.Decimal(1.23) // именно float/decimal
matchers.S("USD") // exact string "USD"
matchers.Regex("ABC123", `^[A-Z]+\d+$`)
matchers.UUID() // любой UUID v4
matchers.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.

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 выдаёт строго такое же сообщение.

Сравнивать сырые байты 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))
}
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.

ПриёмВремя прогона доПослеЭффект
Shared container на suite120s60sконтейнер не пересоздается
TRUNCATE вместо drop schema60s35sмиграции 1 раз
Parallel (per-schema)35s20st.Parallel()
Redpanda вместо Kafka20s10sстарт быстрее
Cache Docker images10s8sdocker pull в CI cache
  • 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.
  • Параллелизм только если изоляция гарантирована.
  • 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 для асинхронной коммуникации.

  1. testcontainers-go documentation — официально.
  2. testcontainers modules — список готовых модулей.
  3. Pact: Consumer-driven contract testing — официальная документация.
  4. pact-foundation/pact-go v2 — Go-клиент.
  5. Buf: breaking change detection — protobuf compatibility.
  6. testify/suite — suite helpers.
  7. Redpanda for testing — Kafka-compatible альтернатива.
  8. toxiproxy — chaos между app и зависимостями.
  9. «Testing Microservices» — Cindy Sridharan — про контрактное тестирование.
  10. «Goldenfiles» pattern (Mitchell Hashimoto) — про сравнение output.