Testing de integração com testcontainers-go
1. Introdução aos testes de integração em Go
Em aplicações Go reais, testes unitários isolam funções e métodos, mas não garantem que o sistema funcione corretamente quando integrado a bancos de dados, filas de mensagens ou caches. Testes de integração preenchem essa lacuna, validando a comunicação entre componentes reais.
Enquanto testes unitários focam em lógica isolada e testes end-to-end cobrem fluxos completos (muitas vezes em ambientes de staging), os testes de integração se concentram em camadas específicas, como repositórios ou serviços que dependem de recursos externos. O testcontainers-go surge como solução elegante: ele gerencia containers Docker programaticamente, permitindo que você suba dependências reais (PostgreSQL, Redis, RabbitMQ) durante os testes e as destrua ao final, sem poluir seu ambiente de desenvolvimento.
2. Configurando o ambiente de teste
Para começar, instale a biblioteca:
go get github.com/testcontainers/testcontainers-go
Requisitos: Docker em execução e contexto configurado. O testcontainers-go se comunica com o daemon Docker via socket padrão.
Estrutura de diretórios recomendada:
projeto/
├── internal/
│ └── repository/
│ ├── user_repository.go
│ └── user_repository_test.go
└── integration_test.go (opcional, com build tags)
Use build tags para separar testes de integração:
//go:build integration
// +build integration
package repository_test
Execute com: go test -tags=integration ./...
3. Primeiro container: PostgreSQL para testes
Vamos criar um container PostgreSQL dinâmico:
package repository_test
import (
"context"
"database/sql"
"testing"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
)
func TestPostgresContainer(t *testing.T) {
ctx := context.Background()
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)
endpoint, err := postgres.Endpoint(ctx, "5432")
if err != nil {
t.Fatal(err)
}
dsn := "postgres://test:test@" + endpoint + "/testdb?sslmode=disable"
db, err := sql.Open("postgres", dsn)
if err != nil {
t.Fatal(err)
}
defer db.Close()
// Executa migrations
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL
)`)
if err != nil {
t.Fatal(err)
}
t.Log("Container PostgreSQL rodando em:", endpoint)
}
A função wait.ForLog aguarda até que o PostgreSQL emita o log de prontidão, garantindo que a conexão seja estabelecida com segurança.
4. Gerenciando ciclo de vida do container
Para setups mais organizados, use TestMain:
package repository_test
import (
"context"
"database/sql"
"os"
"testing"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
)
var testDB *sql.DB
func TestMain(m *testing.M) {
ctx := context.Background()
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 {
os.Exit(1)
}
defer postgres.Terminate(ctx)
endpoint, _ := postgres.Endpoint(ctx, "5432")
dsn := "postgres://test:test@" + endpoint + "/testdb?sslmode=disable"
testDB, _ = sql.Open("postgres", dsn)
code := m.Run()
os.Exit(code)
}
Para timeouts, use context.WithTimeout e verifique logs com postgres.FollowOutput() em cenários de debug.
5. Testando repositórios com banco de dados real
Implemente um repositório simples:
// internal/repository/user_repository.go
package repository
import "database/sql"
type User struct {
ID int
Name string
}
type UserRepository struct {
db *sql.DB
}
func NewUserRepository(db *sql.DB) *UserRepository {
return &UserRepository{db: db}
}
func (r *UserRepository) Create(name string) (int, error) {
var id int
err := r.db.QueryRow("INSERT INTO users (name) VALUES ($1) RETURNING id", name).Scan(&id)
return id, err
}
func (r *UserRepository) FindByID(id int) (*User, error) {
var u User
err := r.db.QueryRow("SELECT id, name FROM users WHERE id = $1", id).Scan(&u.ID, &u.Name)
if err != nil {
return nil, err
}
return &u, nil
}
Teste de integração:
// internal/repository/user_repository_test.go
package repository_test
import (
"testing"
"github.com/stretchr/testify/require"
"meuprojeto/internal/repository"
)
func TestUserRepository_CreateAndFind(t *testing.T) {
repo := repository.NewUserRepository(testDB)
id, err := repo.Create("Alice")
require.NoError(t, err)
require.Greater(t, id, 0)
user, err := repo.FindByID(id)
require.NoError(t, err)
require.Equal(t, "Alice", user.Name)
}
func TestUserRepository_FindByID_NotFound(t *testing.T) {
repo := repository.NewUserRepository(testDB)
_, err := repo.FindByID(999)
require.Error(t, err)
}
6. Containers para Redis, RabbitMQ e outras dependências
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)
endpoint, _ := redisC.Endpoint(ctx, "6379")
// Use go-redis com addr := endpoint
RabbitMQ:
rabbitReq := testcontainers.ContainerRequest{
Image: "rabbitmq:3-management-alpine",
ExposedPorts: []string{"5672/tcp", "15672/tcp"},
WaitingFor: wait.ForLog("started TCP listener on"),
}
Para múltiplos containers, inicie-os no TestMain e compartilhe as conexões.
7. Boas práticas e padrões avançados
Container personalizado com GenericContainer:
customReq := testcontainers.GenericContainerRequest{
ContainerRequest: testcontainers.ContainerRequest{
Image: "minha-imagem:1.0",
Cmd: []string{"--config", "/etc/app/config.yml"},
},
Started: true,
}
Isolamento entre testes: prefira recriar o container para cada pacote de teste (via TestMain), mas para testes individuais que exigem isolamento, use sync.Once para compartilhar um container entre múltiplos testes do mesmo pacote:
var (
once sync.Once
globalDB *sql.DB
)
func getTestDB(t *testing.T) *sql.DB {
once.Do(func() {
// setup do container e banco
})
return globalDB
}
Reset de dados: para evitar recriação, execute TRUNCATE ou DELETE entre testes, mas cuidado com concorrência.
8. Integração com CI/CD e conclusão
No GitHub Actions, garanta que o Docker esteja disponível:
- name: Testes de integração
run: go test -tags=integration -v ./...
A diferença principal entre go test -tags=integration e execução local é que em CI você precisa de um runner com Docker (ubuntu-latest já inclui). Use testcontainers-go com Reaper (container auxiliar para limpeza) que funciona nativamente.
Resumo das vantagens:
- Dependências reais, sem mocks frágeis
- Portabilidade: mesma stack em dev e CI
- Isolamento total: containers são descartados após os testes
- Suporte a múltiplos bancos e serviços
Para evoluir, explore o módulo de mock do testcontainers-go para simular falhas de rede ou use configurações avançadas como testcontainers.WithNetwork para conectar containers entre si.
Referências
- Documentação oficial do testcontainers-go — Guia completo de instalação, quickstart e referência da API.
- testcontainers-go no GitHub — Repositório oficial com exemplos, issues e roadmap.
- Testing Go Applications with Testcontainers (blog oficial) — Artigo introdutório com exemplos práticos de PostgreSQL e Redis.
- How to Write Integration Tests in Go with Testcontainers (TutorialsPoint) — Passo a passo com código comentado e boas práticas.
- Integration Testing in Go with Testcontainers (Alex Edwards) — Tutorial focado em aplicações web e banco de dados.
- Usando Testcontainers para testes de integração em Go (Medium Brasil) — Artigo em português com exemplos de múltiplos containers.
- testcontainers-go: módulo de banco de dados — Documentação específica para PostgreSQL, MySQL, SQL Server e outros bancos.