Testes em Go: Do Unitário ao E2E – Estratégias para código confiável

Código sem teste é código legado em potencial. Você pode ter a arquitetura mais linda do mundo, os melhores padrões de projeto, mas sem testes você nunca saberá se uma mudança simples quebrou algo crítico. Em Go, a filosofia de simplicidade se estende aos testes: a biblioteca padrão já oferece ferramentas poderosas, e boas práticas como interfaces pequenas tornam o código naturalmente testável.

Neste post, vamos explorar uma estratégia completa de testes em Go, desde o unitário até o end-to-end, passando por integração, benchmarks e testes de concorrência. Tudo com exemplos práticos e armadilhas para evitar.

Por que testar em Go é diferente (e melhor)

Diferente de linguagens dinâmicas (Python, Ruby, JavaScript), Go tem tipagem estática e compilação. Isso já elimina classes inteiras de bugs. Mas não é suficiente. O que torna Go especial para testes:

  • Pacote testing nativo – sem necessidade de frameworks externos.
  • Interfaces pequenas – facilitam criação de mocks e stubs.
  • Suporte a testes de tabela (table-driven tests) idiomático.
  • Benchmarks integrados – meça performance junto com correção.
  • Testes de concorrência com -race detector de data races.

1. Testes Unitários: a base de tudo

Testes unitários verificam uma unidade isolada (função, método, struct). Em Go, crie arquivos *_test.go no mesmo pacote.

Exemplo simples

// math.go
package math

func Soma(a, b int) int {
    return a + b
}

// math_test.go
package math

import "testing"

func TestSoma(t *testing.T) {
    resultado := Soma(2, 3)
    esperado := 5
    if resultado != esperado {
        t.Errorf("Soma(2,3) = %d; esperado %d", resultado, esperado)
    }
}

Testes de tabela (table-driven tests) – padrão Go

func TestSoma(t *testing.T) {
    testes := []struct {
        nome     string
        a, b     int
        esperado int
    }{
        {"positivos", 2, 3, 5},
        {"negativos", -1, -2, -3},
        {"zero", 0, 0, 0},
    }

    for _, tt := range testes {
        t.Run(tt.nome, func(t *testing.T) {
            resultado := Soma(tt.a, tt.b)
            if resultado != tt.esperado {
                t.Errorf("Soma(%d, %d) = %d; esperado %d", tt.a, tt.b, resultado, tt.esperado)
            }
        })
    }
}

Mocks com interfaces pequenas

Go não tem mocks embutidos, mas interfaces resolvem.

// repository.go
type UserRepository interface {
    FindByID(ctx context.Context, id string) (*User, error)
}

type UserService struct {
    repo UserRepository
}

func (s *UserService) GetUser(ctx context.Context, id string) (*User, error) {
    return s.repo.FindByID(ctx, id)
}

Teste com mock:

type mockUserRepository struct {
    user *User
    err  error
}

func (m *mockUserRepository) FindByID(ctx context.Context, id string) (*User, error) {
    return m.user, m.err
}

func TestGetUser(t *testing.T) {
    mock := &mockUserRepository{
        user: &User{ID: "1", Name: "Alice"},
    }
    svc := UserService{repo: mock}
    user, err := svc.GetUser(context.Background(), "1")
    if err != nil {
        t.Fatal(err)
    }
    if user.Name != "Alice" {
        t.Errorf("esperado Alice, got %s", user.Name)
    }
}

Ferramentas de mock: Se você precisa de mocks mais sofisticados, use gomock (da Uber) ou mockery (gera mocks a partir de interfaces).

2. Testes de Integração: verifique a interação entre componentes

Testes de integração envolvem dependências externas (banco, API, fila). Use o pacote testing com a flag -integration para separar da execução rápida.

Exemplo com banco PostgreSQL via testcontainers

// go test -tags=integration

//go:build integration

package main

import (
    "testing"
    "github.com/testcontainers/testcontainers-go"
    "github.com/testcontainers/testcontainers-go/modules/postgres"
)

func TestUserRepository_Integration(t *testing.T) {
    ctx := context.Background()
    postgresContainer, err := postgres.Run(ctx, "postgres:15-alpine",
        postgres.WithDatabase("testdb"),
        postgres.WithUsername("test"),
        postgres.WithPassword("test"),
    )
    if err != nil {
        t.Fatal(err)
    }
    defer postgresContainer.Terminate(ctx)

    connStr, _ := postgresContainer.ConnectionString(ctx)
    // use connStr para conectar e testar
}

Dica: Separe testes unitários (rápidos, sem dependências) dos testes de integração (lentos). Execute unitários em cada push, integração no CI antes do merge.

3. Testes End-to-End (E2E): o cenário completo

E2E testa o sistema como um todo, geralmente via chamadas HTTP ou CLI. Use o próprio servidor em modo de teste.

func TestE2E_CreateOrder(t *testing.T) {
    // Inicia servidor HTTP de teste
    server := httptest.NewServer(router())
    defer server.Close()

    // Faz requisição
    resp, err := http.Post(server.URL+"/orders", "application/json", 
        strings.NewReader(`{"product":"book","quantity":2}`))
    if err != nil {
        t.Fatal(err)
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusCreated {
        t.Errorf("esperado 201, got %d", resp.StatusCode)
    }
}

Para testes mais complexos, use httptest.NewRecorder para simular requisições sem rede.

4. Testes de Benchmark: meça performance

O pacote testing também roda benchmarks. Use go test -bench=.

func BenchmarkSoma(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Soma(10, 20)
    }
}

Para comparar implementações:

func BenchmarkSomaParallel(b *testing.B) {
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            Soma(10, 20)
        }
    })
}

Útil: Use benchmarks para evitar regressões de performance em funções críticas.

5. Testes de Concorrência: detecte data races

Go tem detector de races embutido: go test -race. Ele identifica acessos concorrentes não sincronizados à mesma variável.

func TestConcurrentAccess(t *testing.T) {
    var counter int
    var wg sync.WaitGroup
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            counter++ // race condition!
        }()
    }
    wg.Wait()
    // O detector -race vai apontar o problema
}

Correção: Use sync.Mutex, atomic ou canais.

6. Cobertura de testes (code coverage)

Meça a cobertura:

go test -cover
go test -coverprofile=coverage.out
go tool cover -html=coverage.out

Meta: 70%-80% para código crítico; 100% nem sempre é prático nem necessário. Priorize caminhos críticos e bordas.

7. Estratégias avançadas

Testes com t.Parallel() para acelerar

func TestSomething(t *testing.T) {
    t.Parallel()
    // teste isolado
}

Subtests com t.Run() (já mostrado)

Organizam e permitem execução seletiva: go test -run TestSoma/positivos.

Testes de fuzzing (Go 1.18+)

Gera entradas aleatórias para encontrar bugs.

func FuzzSoma(f *testing.F) {
    f.Add(0, 0)
    f.Fuzz(func(t *testing.T, a, b int) {
        resultado := Soma(a, b)
        // invariante: soma é comutativa
        if Soma(b, a) != resultado {
            t.Errorf("não comutativa")
        }
    })
}

CI/CD: rodando testes automaticamente

Em GitHub Actions, GitLab CI ou similar:

test:
  run: go test -race -cover ./...

Sempre rode -race no pipeline. Execute testes de integração separadamente (ex: com banco de testes).

Erros comuns em testes Go

ErroConsequênciaSolução
Esquecer -raceData races vão para produçãoInclua no CI
Testes lentos com dependências reaisCI demora horasUse mocks e testcontainers
Cobertura alta mas sem asserts significativosFalsa segurançaTeste comportamentos, não linhas
Testes frágeis (mudança de ordem quebra)CI falha sem motivoUse assert independentes

Conclusão

Testar em Go é direto e poderoso. A biblioteca padrão cobre unitários, benchmarks e fuzzing. Com interfaces pequenas, mocks são triviais. Com -race, você pega problemas de concorrência. Com testcontainers, testes de integração confiáveis. Adote uma pirâmide: muitos testes unitários, menos de integração, poucos E2E.

Na Jacobus Software, testes são parte indissociável do desenvolvimento. Não entregamos código sem cobertura significativa e sem passar no detector de races. O resultado: sistemas que evoluem com confiança.


🧪 Quer elevar a qualidade do seu código Go?

Nossos especialistas implementam suites de teste, treinam seu time em TDD e configuram pipelines de CI com cobertura e detecção de races.

👉 Fale com a Jacobus Software

Rolar para cima