Go

Guia Completo de Testes de Integração em Go: Banco Real com testcontainers-go

14 min de leitura

Guia Completo de Testes de Integração em Go: Banco Real com testcontainers-go

Por que Testes de Integração Importam em Go Testes de integração são diferentes dos testes unitários. Enquanto um teste unitário valida uma função isolada com mocks e dados em memória, um teste de integração verifica o comportamento real do sistema quando múltiplos componentes trabalham juntos. Em Go, isso significa testar sua aplicação contra um banco de dados real, não contra um simulado. A razão pela qual isso é crítico: bugs de integração nunca aparecem em testes unitários. Você pode ter validações perfeitas em sua camada de acesso a dados, mas descobrir em produção que sua query não funciona com a versão específica do PostgreSQL. Com testcontainers-go, você executa esses testes contra um container Docker real do banco — o mesmo que rodará em produção — sem poluir seu ambiente local ou exigir uma infraestrutura de teste complexa. Conceitos Fundamentais de testcontainers-go O que é testcontainers-go? testcontainers-go é uma biblioteca que permite provisionar e gerenciar containers Docker durante testes de forma

<h2>Por que Testes de Integração Importam em Go</h2>

<p>Testes de integração são diferentes dos testes unitários. Enquanto um teste unitário valida uma função isolada com mocks e dados em memória, um teste de integração verifica o comportamento real do sistema quando múltiplos componentes trabalham juntos. Em Go, isso significa testar sua aplicação contra um banco de dados real, não contra um simulado.</p>

<p>A razão pela qual isso é crítico: bugs de integração nunca aparecem em testes unitários. Você pode ter validações perfeitas em sua camada de acesso a dados, mas descobrir em produção que sua query não funciona com a versão específica do PostgreSQL. Com testcontainers-go, você executa esses testes contra um container Docker real do banco — o mesmo que rodará em produção — sem poluir seu ambiente local ou exigir uma infraestrutura de teste complexa.</p>

<h2>Conceitos Fundamentais de testcontainers-go</h2>

<h3>O que é testcontainers-go?</h3>

<p>testcontainers-go é uma biblioteca que permite provisionar e gerenciar containers Docker durante testes de forma programática. Você escreve código Go que sobe um PostgreSQL (ou MySQL, MongoDB, Redis, etc.) em um container, executa seus testes contra ele, e derruba o container automaticamente ao final. Sem shell scripts, sem configuração manual, sem portas hardcoded.</p>

<p>Sob o capô, a biblioteca se comunica com o daemon Docker via API, cria uma rede isolada para os containers, expõe portas aleatórias para evitar conflitos, e fornece uma API limpa para sua linguagem de teste. Você não precisa entender Docker profundamente — a biblioteca abstrai a complexidade.</p>

<h3>Por que não usar um banco em arquivo (SQLite)?</h3>

<p>Para testes unitários rápidos, SQLite é ótimo. Mas em produção você usa PostgreSQL ou MySQL. O SQLite tem dialeto SQL ligeiramente diferente, comportamentos distintos em transações e concorrência, e indexes que funcionam diferente. Testar contra SQLite e ir para produção com PostgreSQL é como testar sua API com um cliente mock e esperar que funcione com requisições reais. testcontainers-go elimina essa discrepância.</p>

<h2>Configurando seu Primeiro Teste com testcontainers-go</h2>

<h3>Instalação e Dependências</h3>

<p>Você precisará do Docker instalado e rodando em sua máquina. A biblioteca Go é instalada como qualquer outra dependência:</p>

<pre><code class="language-bash">go get github.com/testcontainers/testcontainers-go

go get github.com/testcontainers/testcontainers-go/wait

go get github.com/lib/pq # driver PostgreSQL</code></pre>

<h3>Estrutura Básica: Conectando ao PostgreSQL</h3>

<p>Aqui está um teste real que provisiona um PostgreSQL, cria uma tabela, insere dados, e valida:</p>

<pre><code class="language-go">package main

import (

&quot;context&quot;

&quot;database/sql&quot;

&quot;fmt&quot;

&quot;testing&quot;

&quot;github.com/testcontainers/testcontainers-go&quot;

&quot;github.com/testcontainers/testcontainers-go/wait&quot;

_ &quot;github.com/lib/pq&quot;

)

func TestPostgresIntegration(t *testing.T) {

ctx := context.Background()

// Define o container PostgreSQL

req := testcontainers.ContainerRequest{

Image: &quot;postgres:15-alpine&quot;,

ExposedPorts: []string{&quot;5432/tcp&quot;},

Env: map[string]string{

&quot;POSTGRES_USER&quot;: &quot;testuser&quot;,

&quot;POSTGRES_PASSWORD&quot;: &quot;testpass&quot;,

&quot;POSTGRES_DB&quot;: &quot;testdb&quot;,

},

WaitingFor: wait.ForLog(&quot;database system is ready to accept connections&quot;),

}

// Cria e inicia o container

container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{

ContainerRequest: req,

Started: true,

})

if err != nil {

t.Fatalf(&quot;Erro ao iniciar container: %v&quot;, err)

}

defer container.Terminate(ctx)

// Obtém a porta mapeada (importante: não é sempre 5432)

port, err := container.MappedPort(ctx, &quot;5432&quot;)

if err != nil {

t.Fatalf(&quot;Erro ao obter porta: %v&quot;, err)

}

// Constrói a string de conexão dinamicamente

dsn := fmt.Sprintf(&quot;postgres://testuser:testpass@localhost:%s/testdb?sslmode=disable&quot;,

port.Port())

// Aguarda a conexão estar pronta

db, err := sql.Open(&quot;postgres&quot;, dsn)

if err != nil {

t.Fatalf(&quot;Erro ao abrir conexão: %v&quot;, err)

}

defer db.Close()

// Aguarda o banco estar realmente pronto

if err := db.PingContext(ctx); err != nil {

t.Fatalf(&quot;Erro ao fazer ping no banco: %v&quot;, err)

}

// Cria tabela de teste

_, err = db.ExecContext(ctx, `

CREATE TABLE users (

id SERIAL PRIMARY KEY,

name VARCHAR(100) NOT NULL,

email VARCHAR(100) UNIQUE NOT NULL

)

`)

if err != nil {

t.Fatalf(&quot;Erro ao criar tabela: %v&quot;, err)

}

// Insere dados de teste

_, err = db.ExecContext(ctx,

&quot;INSERT INTO users (name, email) VALUES ($1, $2)&quot;,

&quot;João Silva&quot;, &quot;joao@example.com&quot;)

if err != nil {

t.Fatalf(&quot;Erro ao inserir dados: %v&quot;, err)

}

// Valida os dados

var name, email string

err = db.QueryRowContext(ctx, &quot;SELECT name, email FROM users WHERE id = 1&quot;).

Scan(&amp;name, &amp;email)

if err != nil {

t.Fatalf(&quot;Erro ao consultar dados: %v&quot;, err)

}

if name != &quot;João Silva&quot; || email != &quot;joao@example.com&quot; {

t.Errorf(&quot;Dados retornados incorretos: %s, %s&quot;, name, email)

}

t.Log(&quot;✓ Teste passou com sucesso&quot;)

}</code></pre>

<p>Execute com <code>go test -v</code>. Na primeira execução, Docker baixará a imagem do PostgreSQL (alguns minutos). Nas próximas, o teste rodará em segundos.</p>

<h3>O que Aconteceu Aqui</h3>

<p>A <code>ContainerRequest</code> define qual imagem usar, quais variáveis de ambiente, e qual critério de espera (<code>WaitingFor</code>). O <code>wait.ForLog</code> monitora os logs do container até encontrar a string de sucesso — assim você evita raceconditions onde a porta está aberta mas o banco ainda não aceitava conexões.</p>

<p><code>GenericContainer</code> cria e inicia o container. Você obtém a porta <strong>mapeada</strong> (não a original), porque Docker atribui portas aleatórias para evitar conflitos. Sem isso, dois testes rodando em paralelo usariam a mesma porta e falhariam.</p>

<h2>Padrões Avançados: Fixtures, Helpers e Múltiplos Containers</h2>

<h3>Criando um Helper Reutilizável</h3>

<p>Repetir toda aquela boilerplate em cada teste é tedioso. Crie um helper:</p>

<pre><code class="language-go">package tests

import (

&quot;context&quot;

&quot;database/sql&quot;

&quot;fmt&quot;

&quot;testing&quot;

&quot;github.com/testcontainers/testcontainers-go&quot;

&quot;github.com/testcontainers/testcontainers-go/wait&quot;

_ &quot;github.com/lib/pq&quot;

)

// PostgresContainer encapsula a lógica de setup

type PostgresContainer struct {

Container testcontainers.Container

DB *sql.DB

DSN string

}

// NewPostgresContainer cria um novo container PostgreSQL

func NewPostgresContainer(ctx context.Context, t testing.T) PostgresContainer {

req := testcontainers.ContainerRequest{

Image: &quot;postgres:15-alpine&quot;,

ExposedPorts: []string{&quot;5432/tcp&quot;},

Env: map[string]string{

&quot;POSTGRES_USER&quot;: &quot;testuser&quot;,

&quot;POSTGRES_PASSWORD&quot;: &quot;testpass&quot;,

&quot;POSTGRES_DB&quot;: &quot;testdb&quot;,

},

WaitingFor: wait.ForLog(&quot;database system is ready to accept connections&quot;),

}

container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{

ContainerRequest: req,

Started: true,

})

if err != nil {

t.Fatalf(&quot;Erro ao iniciar PostgreSQL: %v&quot;, err)

}

port, err := container.MappedPort(ctx, &quot;5432&quot;)

if err != nil {

t.Fatalf(&quot;Erro ao obter porta: %v&quot;, err)

}

dsn := fmt.Sprintf(&quot;postgres://testuser:testpass@localhost:%s/testdb?sslmode=disable&quot;,

port.Port())

db, err := sql.Open(&quot;postgres&quot;, dsn)

if err != nil {

t.Fatalf(&quot;Erro ao conectar: %v&quot;, err)

}

if err := db.PingContext(ctx); err != nil {

t.Fatalf(&quot;Erro ao fazer ping: %v&quot;, err)

}

return &amp;PostgresContainer{

Container: container,

DB: db,

DSN: dsn,

}

}

// Cleanup para e remove o container

func (pc *PostgresContainer) Cleanup(ctx context.Context) error {

if pc.DB != nil {

pc.DB.Close()

}

if pc.Container != nil {

return pc.Container.Terminate(ctx)

}

return nil

}</code></pre>

<p>Agora seus testes ficam muito mais limpos:</p>

<pre><code class="language-go">func TestUserRepository(t *testing.T) {

ctx := context.Background()

pg := NewPostgresContainer(ctx, t)

defer pg.Cleanup(ctx)

// Seu teste aqui

_, err := pg.DB.ExecContext(ctx, &quot;INSERT INTO users (name, email) VALUES ($1, $2)&quot;,

&quot;Maria&quot;, &quot;maria@test.com&quot;)

if err != nil {

t.Fatal(err)

}

}</code></pre>

<h3>Testando com Dados Pré-carregados</h3>

<p>Muitas vezes você precisa de dados de teste já populados. Use migrations ou SQL files:</p>

<pre><code class="language-go">func (pc PostgresContainer) RunMigrations(ctx context.Context, t testing.T) {

migrations := []string{

`CREATE TABLE users (

id SERIAL PRIMARY KEY,

name VARCHAR(100) NOT NULL,

email VARCHAR(100) UNIQUE NOT NULL

)`,

`CREATE TABLE orders (

id SERIAL PRIMARY KEY,

user_id INT NOT NULL REFERENCES users(id),

total DECIMAL(10, 2) NOT NULL

)`,

}

for _, migration := range migrations {

if _, err := pc.DB.ExecContext(ctx, migration); err != nil {

t.Fatalf(&quot;Erro na migração: %v&quot;, err)

}

}

}

func (pc PostgresContainer) SeedTestData(ctx context.Context, t testing.T) {

// Insere dados iniciais

_, err := pc.DB.ExecContext(ctx, `

INSERT INTO users (name, email) VALUES

(&#039;Alice&#039;, &#039;alice@test.com&#039;),

(&#039;Bob&#039;, &#039;bob@test.com&#039;),

(&#039;Charlie&#039;, &#039;charlie@test.com&#039;)

`)

if err != nil {

t.Fatalf(&quot;Erro ao inserir dados de teste: %v&quot;, err)

}

}</code></pre>

<h3>Múltiplos Containers: PostgreSQL + Redis</h3>

<p>Às vezes você precisa testar com vários serviços simultaneamente:</p>

<pre><code class="language-go">func TestUserServiceWithRedisCache(t *testing.T) {

ctx := context.Background()

// PostgreSQL

pgReq := testcontainers.ContainerRequest{

Image: &quot;postgres:15-alpine&quot;,

ExposedPorts: []string{&quot;5432/tcp&quot;},

Env: map[string]string{

&quot;POSTGRES_USER&quot;: &quot;testuser&quot;,

&quot;POSTGRES_PASSWORD&quot;: &quot;testpass&quot;,

&quot;POSTGRES_DB&quot;: &quot;testdb&quot;,

},

WaitingFor: wait.ForLog(&quot;database system is ready to accept connections&quot;),

}

pgContainer, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{

ContainerRequest: pgReq,

Started: true,

})

if err != nil {

t.Fatalf(&quot;Erro ao iniciar PostgreSQL: %v&quot;, err)

}

defer pgContainer.Terminate(ctx)

// Redis

redisReq := testcontainers.ContainerRequest{

Image: &quot;redis:7-alpine&quot;,

ExposedPorts: []string{&quot;6379/tcp&quot;},

WaitingFor: wait.ForLog(&quot;Ready to accept connections&quot;),

}

redisContainer, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{

ContainerRequest: redisReq,

Started: true,

})

if err != nil {

t.Fatalf(&quot;Erro ao iniciar Redis: %v&quot;, err)

}

defer redisContainer.Terminate(ctx)

// Obtém portas

pgPort, _ := pgContainer.MappedPort(ctx, &quot;5432&quot;)

redisPort, _ := redisContainer.MappedPort(ctx, &quot;6379&quot;)

t.Logf(&quot;PostgreSQL rodando na porta %s&quot;, pgPort.Port())

t.Logf(&quot;Redis rodando na porta %s&quot;, redisPort.Port())

// Seu teste aqui usa ambos os containers

}</code></pre>

<h2>Executando e Depurando Seus Testes</h2>

<h3>Rodando Testes em Paralelo</h3>

<p>Por padrão, Go testa funções <code>TestXxx</code> sequencialmente. Para testes de integração com containers, paralelo economiza tempo:</p>

<pre><code class="language-bash">go test -v -parallel 4</code></pre>

<p>Cada teste recebe seu próprio container isolado (mesma imagem, diferentes IDs), então não há conflito. testcontainers-go gerencia isso automaticamente.</p>

<h3>Visualizando Logs do Container</h3>

<p>Se um teste falha, os logs do container são valiosos. A biblioteca oferece um método para capturá-los:</p>

<pre><code class="language-go">func TestWithLogs(t *testing.T) {

ctx := context.Background()

req := testcontainers.ContainerRequest{

Image: &quot;postgres:15-alpine&quot;,

ExposedPorts: []string{&quot;5432/tcp&quot;},

// ... rest of config

}

container, _ := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{

ContainerRequest: req,

Started: true,

})

defer container.Terminate(ctx)

// Seu teste...

if t.Failed() {

logs, _ := container.Logs(ctx)

t.Logf(&quot;Container logs:\n%s&quot;, logs)

}

}</code></pre>

<h3>Mantendo o Container Rodando para Debug</h3>

<p>Para debugar, às vezes é útil manter o container vivo após o teste falhar:</p>

<pre><code class="language-go">// Descomente a linha defer para permitir inspeção manual

// defer container.Terminate(ctx)

// Aguarde para inspeccionar manualmente

// time.Sleep(30 * time.Second)</code></pre>

<p>Assim você pode <code>docker ps</code> e <code>docker exec</code> para investigar o estado do banco manualmente.</p>

<h2>Conclusão</h2>

<p>Você aprendeu três coisas fundamentais sobre testes de integração com Go e testcontainers-go:</p>

<ol>

<li><strong>Testes contra containers reais eliminam surpresas de produção</strong> — testar contra PostgreSQL em um container evita bugs que não aparecem em testes unitários com mocks. O banco que você testa é o mesmo que roda em produção.</li>

</ol>

<ol>

<li><strong>testcontainers-go abstrai a complexidade de Docker</strong> — você escreve código Go limpo, não scripts shell ou configurações manuais. A biblioteca provisiona, aguarda readiness, mapeia portas aleatoriedade e faz limpeza automaticamente.</li>

</ol>

<ol>

<li><strong>Padrões como helpers reutilizáveis escalam bem</strong> — encapsular a lógica de setup em métodos torna seus testes legíveis, manuteníveis e reutilizáveis entre múltiplos testes e múltiplos bancos (PostgreSQL, Redis, etc.).</li>

</ol>

<p>De aqui para frente, procure explorar waitingFor customizados (nem sempre um log é suficiente), reutilizar containers entre testes para ganho de performance quando apropriado, e integrar isso em seu pipeline CI/CD (GitHub Actions, GitLab CI, etc. têm suporte nativo a Docker).</p>

<h2>Referências</h2>

<ul>

<li><a href="https://golang.testcontainers.org/" target="_blank" rel="noopener noreferrer">testcontainers-go Documentação Oficial</a></li>

<li><a href="https://pkg.go.dev/database/sql" target="_blank" rel="noopener noreferrer">Go database/sql Package</a></li>

<li><a href="https://hub.docker.com/_/postgres" target="_blank" rel="noopener noreferrer">PostgreSQL Docker Image</a></li>

<li><a href="https://www.youtube.com/watch?v=ndmB0bHoyjc" target="_blank" rel="noopener noreferrer">Testing in Go: The Basics</a></li>

<li><a href="https://golang.org/doc/effective_go#generics" target="_blank" rel="noopener noreferrer">Table-Driven Tests in Go</a></li>

</ul>

<p>&lt;!-- FIM --&gt;</p>

Comentários

Mais em Go

Boas Práticas de CQRS e Event Sourcing em Go: Implementação Prática para Times Ágeis
Boas Práticas de CQRS e Event Sourcing em Go: Implementação Prática para Times Ágeis

Entendendo CQRS: O Padrão de Separação de Responsabilidades CQRS significa Co...

Stack vs Heap em Go: Escape Analysis e Alocação Eficiente: Do Básico ao Avançado
Stack vs Heap em Go: Escape Analysis e Alocação Eficiente: Do Básico ao Avançado

Fundamentos de Stack e Heap em Go A memória em qualquer programa está organiz...

O que Todo Dev Deve Saber sobre Build e Cross-Compilation em Go: Binários para Múltiplas Plataformas
O que Todo Dev Deve Saber sobre Build e Cross-Compilation em Go: Binários para Múltiplas Plataformas

Build e Cross-Compilation em Go: Dominando Binários para Múltiplas Plataforma...