
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
testingnativo – 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
-racedetector 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
| Erro | Consequência | Solução |
|---|---|---|
Esquecer -race | Data races vão para produção | Inclua no CI |
| Testes lentos com dependências reais | CI demora horas | Use mocks e testcontainers |
| Cobertura alta mas sem asserts significativos | Falsa segurança | Teste comportamentos, não linhas |
| Testes frágeis (mudança de ordem quebra) | CI falha sem motivo | Use 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.
