Testes de Unidade em Go: Estrutura e Melhores Práticas
Testes em Go: dos conceitos básicos aos padrões avançados
O pacote de testes nativo do Go oferece uma poderosa e minimalista estrutura para escrever testes unitários sem dependências externas. Aqui estão os fundamentos de teste, a estrutura do projeto e padrões avançados para criar aplicações Go confiáveis.

Por que Testar é Importante em Go
A filosofia do Go enfatiza simplicidade e confiabilidade. A biblioteca padrão inclui o pacote testing, tornando os testes unitários cidadãos de primeira classe no ecossistema Go. Código Go bem testado melhora a manutenibilidade, detecta bugs precocemente e oferece documentação por meio de exemplos. Se você é novo no Go, confira nossa Lista de Referências Rápidas do Go para uma referência rápida dos fundamentos da linguagem.
Principais benefícios dos testes em Go:
- Suporte nativo: Nenhuma estrutura externa necessária
- Execução rápida: Execução de testes concorrentes por padrão
- Sintaxe simples: Código boilerplate mínimo
- Ferramentas ricas: Relatórios de cobertura, benchmarks e perfis
- Amigável para CI/CD: Integração fácil com pipelines automatizados
Estrutura de Projeto para Testes em Go
Os testes em Go vivem junto com o código de produção seguindo uma convenção de nomenclatura clara:
myproject/
├── go.mod
├── main.go
├── calculator.go
├── calculator_test.go
├── utils/
│ ├── helper.go
│ └── helper_test.go
└── models/
├── user.go
└── user_test.go
Convenções principais:
- Arquivos de teste terminam com
_test.go - Testes estão no mesmo pacote que o código (ou usam o sufixo
_testpara testes de caixa preta) - Cada arquivo de origem pode ter um arquivo de teste correspondente
Abordagens de Teste de Pacote
Teste de caixa branca (mesmo pacote):
package calculator
import "testing"
// Pode acessar funções e variáveis não exportadas
Teste de caixa preta (pacote externo):
package calculator_test
import (
"testing"
"myproject/calculator"
)
// Pode acessar apenas funções exportadas (recomendado para APIs públicas)
Estrutura Básica de Teste
Cada função de teste segue este padrão:
package calculator
import "testing"
// A função de teste deve começar com "Test"
func TestAdd(t *testing.T) {
resultado := Add(2, 3)
esperado := 5
if resultado != esperado {
t.Errorf("Add(2, 3) = %d; querendo %d", resultado, esperado)
}
}
Métodos de Testing.T:
t.Error()/t.Errorf(): Marca o teste como falhado, mas continuat.Fatal()/t.Fatalf(): Marca o teste como falhado e para imediatamentet.Log()/t.Logf(): Registra saída (mostrado apenas com a flag-v)t.Skip()/t.Skipf(): Pulou o testet.Parallel(): Executa o teste em paralelo com outros testes paralelos
t.Log é para diagnósticos de teste legíveis para humanos. Em serviços em execução, log/slog e registros amigáveis a JSON geralmente são uma melhor correspondência para agregação e depuração de incidentes. Veja Log Estruturado em Go com slog para Observabilidade e Alertas.
Testes Orientados a Tabela: A Maneira Go
Os testes orientados a tabela são a abordagem idiomática do Go para testar múltiplos cenários. Com generics do Go, você também pode criar auxiliares de teste seguros em tipos que funcionam em diferentes tipos de dados:
func TestCalculate(t *testing.T) {
testes := []struct {
nome string
a, b int
op string
esperado int
querErro bool
}{
{"adição", 2, 3, "+", 5, false},
{"subtração", 5, 3, "-", 2, false},
{"multiplicação", 4, 3, "*", 12, false},
{"divisão", 10, 2, "/", 5, false},
{"divisão por zero", 10, 0, "/", 0, true},
}
for _, tt := range testes {
t.Run(tt.nome, func(t *testing.T) {
resultado, err := Calculate(tt.a, tt.b, tt.op)
if (err != nil) != tt.querErro {
t.Errorf("Calculate() erro = %v, querErro %v", err, tt.querErro)
return
}
if resultado != tt.esperado {
t.Errorf("Calculate(%d, %d, %q) = %d; querendo %d",
tt.a, tt.b, tt.op, resultado, tt.esperado)
}
})
}
}
Vantagens:
- Função de teste única para múltiplos cenários
- Fácil de adicionar novos casos de teste
- Documentação clara do comportamento esperado
- Melhor organização e manutenibilidade de testes
Executando Testes
Comandos Básicos
# Executar testes no diretório atual
go test
# Executar testes com saída verbosa
go test -v
# Executar testes em todos os subdiretórios
go test ./...
# Executar teste específico
go test -run TestAdd
# Executar testes correspondentes ao padrão
go test -run TestCalculate/adição
# Executar testes em paralelo (padrão é GOMAXPROCS)
go test -parallel 4
# Executar testes com tempo limite
go test -timeout 30s
Cobertura de Testes
# Executar testes com cobertura
go test -cover
# Gerar perfil de cobertura
go test -coverprofile=coverage.out
# Visualizar cobertura no navegador
go tool cover -html=coverage.out
# Mostrar cobertura por função
go tool cover -func=coverage.out
# Definir modo de cobertura (set, count, atomic)
go test -covermode=count -coverprofile=coverage.out
Flags Úteis
-short: Executa testes marcados com verificaçõesif testing.Short()-race: Ativa o detector de race conditions (encontra problemas de acesso concorrente)-cpu: Especifica valores de GOMAXPROCS-count n: Executa cada teste n vezes-failfast: Para na primeira falha do teste
Auxiliares de Teste e Setup/Teardown
Funções Auxiliares
Marque funções auxiliares com t.Helper() para melhorar a relatoria de erros:
func assertEqual(t *testing.T, got, want int) {
t.Helper() // Esta linha é relatada como o chamador
if got != want {
t.Errorf("recebido %d, querendo %d", got, want)
}
}
func TestMath(t *testing.T) {
resultado := Add(2, 3)
assertEqual(t, resultado, 5) // Linha de erro aponta aqui
}
Setup e Teardown
func TestMain(m *testing.M) {
// Código de setup aqui
setup()
// Executar testes
codigo := m.Run()
// Código de teardown aqui
teardown()
os.Exit(codigo)
}
Fixtures de Teste
func setupTestCase(t *testing.T) func(t *testing.T) {
t.Log("setup do caso de teste")
return func(t *testing.T) {
t.Log("teardown do caso de teste")
}
}
func TestSomething(t *testing.T) {
teardown := setupTestCase(t)
defer teardown(t)
// Código de teste aqui
}
Mocking e Injeção de Dependências
Mocking Baseado em Interfaces
Ao testar código que interage com bancos de dados, o uso de interfaces facilita a criação de implementações mock. Se você está trabalhando com PostgreSQL em Go, veja nossa comparação de ORMs do Go para escolher a biblioteca de banco de dados certa com boa testabilidade.
// Código de produção
type Database interface {
GetUser(id int) (*User, error)
}
type UserService struct {
db Database
}
func (s *UserService) GetUserName(id int) (string, error) {
usuario, err := s.db.GetUser(id)
if err != nil {
return "", err
}
return usuario.Nome, nil
}
// Código de teste
type MockDatabase struct {
usuarios map[int]*User
}
func (m *MockDatabase) GetUser(id int) (*User, error) {
if usuario, ok := m.usuarios[id]; ok {
return usuario, nil
}
return nil, errors.New("usuário não encontrado")
}
func TestGetUserName(t *testing.T) {
mockDB := &MockDatabase{
usuarios: map[int]*User{
1: {ID: 1, Nome: "Alice"},
},
}
service := &UserService{db: mockDB}
nome, err := service.GetUserName(1)
if err != nil {
t.Fatalf("erro inesperado: %v", err)
}
if nome != "Alice" {
t.Errorf("recebido %s, querendo Alice", nome)
}
}
Bibliotecas de Teste Populares
Testify
A biblioteca de teste Go mais popular para asserções e mocks:
import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
func TestWithTestify(t *testing.T) {
resultado := Add(2, 3)
assert.Equal(t, 5, resultado, "deveriam ser iguais")
assert.NotNil(t, resultado)
}
// Exemplo de mock
type MockDB struct {
mock.Mock
}
func (m *MockDB) GetUser(id int) (*User, error) {
args := m.Called(id)
return args.Get(0).(*User), args.Error(1)
}
Outras Ferramentas
- gomock: Estrutura de mocking do Google com geração de código
- httptest: Biblioteca padrão para testar manipuladores HTTP
- testcontainers-go: Testes de integração com contêineres Docker
- ginkgo/gomega: Estrutura de teste estilo BDD
Ao testar integrações com serviços externos, como modelos de IA, você precisará mockar ou substituir essas dependências. Por exemplo, se você está usando Ollama em Go, considere criar wrappers de interface para tornar seu código mais testável.
Testes de Benchmark
O Go inclui suporte nativo para benchmarks:
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(2, 3)
}
}
// Executar benchmarks
// go test -bench=. -benchmem
A saída mostra iterações por segundo e alocações de memória.
Melhores Práticas
- Escreva testes orientados a tabela: Use o padrão de fatia de structs para múltiplos casos de teste
- Use t.Run para subtestes: Melhor organização e pode executar subtestes seletivamente
- Teste funções exportadas primeiro: Concentre-se no comportamento da API pública
- Mantenha os testes simples: Cada teste deve verificar uma coisa
- Use nomes de teste significativos: Descreva o que está sendo testado e o resultado esperado
- Não teste detalhes de implementação: Teste comportamento, não internos
- Use interfaces para dependências: Facilita o mocking
- Almeje alta cobertura, mas qualidade sobre quantidade: 100% de cobertura não significa livre de bugs
- Execute testes com a flag -race: Detecte problemas de concorrência cedo
- Use TestMain para setup caro: Evite repetir setup em cada teste
Exemplo: Suite de Testes Completa
package user
import (
"errors"
"testing"
)
type User struct {
ID int
Nome string
Email string
}
func ValidateUser(u *User) error {
if u.Nome == "" {
return errors.New("nome não pode estar vazio")
}
if u.Email == "" {
return errors.New("email não pode estar vazio")
}
return nil
}
// Arquivo de teste: user_test.go
func TestValidateUser(t *testing.T) {
testes := []struct {
nome string
usuario *User
querErro bool
msgErro string
}{
{
nome: "usuário válido",
usuario: &User{ID: 1, Nome: "Alice", Email: "alice@exemplo.com"},
querErro: false,
},
{
nome: "nome vazio",
usuario: &User{ID: 1, Nome: "", Email: "alice@exemplo.com"},
querErro: true,
msgErro: "nome não pode estar vazio",
},
{
nome: "email vazio",
usuario: &User{ID: 1, Nome: "Alice", Email: ""},
querErro: true,
msgErro: "email não pode estar vazio",
},
}
for _, tt := range testes {
t.Run(tt.nome, func(t *testing.T) {
err := ValidateUser(tt.usuario)
if (err != nil) != tt.querErro {
t.Errorf("ValidateUser() erro = %v, querErro %v", err, tt.querErro)
return
}
if err != nil && err.Error() != tt.msgErro {
t.Errorf("ValidateUser() mensagem de erro = %v, querendo %v", err.Error(), tt.msgErro)
}
})
}
}
Links Úteis
- Documentação Oficial do Pacote de Testes do Go
- Blog do Go: Testes Orientados a Tabela
- Repositório GitHub do Testify
- Documentação do GoMock
- Aprenda Go com Testes
- Ferramenta de Cobertura de Código Go
- Lista de Referências Rápidas do Go
- Log Estruturado em Go com slog para Observabilidade e Alertas
- Comparando ORMs do Go para PostgreSQL: GORM vs Ent vs Bun vs sqlc
- SDKs do Go para Ollama - comparação com exemplos
- Criando Aplicações CLI em Go com Cobra & Viper
- Generics do Go: Casos de Uso e Padrões
Conclusão
A estrutura de testes do Go fornece tudo o que é necessário para testes unitários abrangentes com configuração mínima. Seguindo idiomas do Go como testes orientados a tabela, usando interfaces para mocking e aproveitando ferramentas nativas, você pode criar suítes de teste mantíveis e confiáveis que crescem junto com sua base de código.
Estas práticas de teste se aplicam a todos os tipos de aplicações Go, desde serviços web até aplicações CLI construídas com Cobra & Viper. Testar ferramentas de linha de comando requer padrões semelhantes com foco adicional em testar entrada/saída e análise de flags.
Comece com testes simples, adicione cobertura gradualmente e lembre-se de que testar é um investimento na qualidade do código e na confiança do desenvolvedor. A ênfase da comunidade Go em testes torna mais fácil manter projetos a longo prazo e colaborar efetivamente com membros da equipe.