O que é Testing em Go?
O testing é o package nativo do Go para escrever e executar testes automatizados. Diferente de muitas linguagens que dependem de frameworks externos (JUnit, pytest, Jest), Go traz um sistema de testes completo e poderoso integrado à linguagem e ao toolchain. Basta criar um arquivo com sufixo _test.go, importar testing e rodar go test.
Essa decisão de design reflete a filosofia do Go: ferramentas essenciais para desenvolvimento de software devem estar disponíveis nativamente, sem dependências externas. O resultado é que todo projeto Go segue as mesmas convenções de teste, facilitando a colaboração entre equipes e a manutenção de código open source.
O package testing fornece tipos como *testing.T para testes unitários, *testing.B para benchmarks e *testing.F para fuzzing. Neste artigo, vamos focar nos testes unitários e suas funcionalidades avançadas.
Escrevendo seu primeiro teste
Um teste em Go é uma func que começa com Test, recebe um parâmetro *testing.T e está em um arquivo _test.go:
// math.go
package math
func Soma(a, b int) int {
return a + b
}
func Divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("divisão por zero")
}
return a / b, nil
}
// 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)
}
}
func TestDivide(t *testing.T) {
resultado, err := Divide(10, 2)
if err != nil {
t.Fatalf("erro inesperado: %v", err)
}
if resultado != 5.0 {
t.Errorf("Divide(10, 2) = %f; esperado 5.0", resultado)
}
}
func TestDividePorZero(t *testing.T) {
_, err := Divide(10, 0)
if err == nil {
t.Error("esperava erro para divisão por zero, mas não recebeu")
}
}
Para executar, basta rodar no terminal:
go test ./...
Métodos de *testing.T
Os métodos mais usados de *testing.T são:
| Método | Comportamento |
|---|---|
t.Error(args...) | Registra falha mas continua o teste |
t.Errorf(format, args...) | Como Error, com formatação |
t.Fatal(args...) | Registra falha e para o teste imediatamente |
t.Fatalf(format, args...) | Como Fatal, com formatação |
t.Log(args...) | Registra informação (visível com go test -v) |
t.Skip(args...) | Pula o teste com uma mensagem |
t.Helper() | Marca a função como helper (melhora stack traces) |
t.Cleanup(func()) | Registra função de limpeza executada ao final |
Table-driven tests
O padrão table-driven tests é a abordagem mais idiomática em Go para testar múltiplos cenários. Você define uma slice de casos de teste e itera sobre eles:
func TestSomaTableDriven(t *testing.T) {
tests := []struct {
nome string
a, b int
esperado int
}{
{"positivos", 2, 3, 5},
{"com zero", 0, 5, 5},
{"negativos", -1, -2, -3},
{"misto", -1, 5, 4},
{"zeros", 0, 0, 0},
{"grandes", 1000000, 2000000, 3000000},
}
for _, tt := range tests {
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)
}
})
}
}
A variável tt é convenção para “table test”. O range itera sobre cada caso, e t.Run cria um subteste nomeado — facilitando identificar qual cenário falhou.
Subtests com t.Run
O método t.Run permite criar subtestes hierárquicos, úteis para organizar testes por categoria ou comportamento:
func TestUsuario(t *testing.T) {
t.Run("Criacao", func(t *testing.T) {
t.Run("com dados validos", func(t *testing.T) {
u, err := NovoUsuario("Maria", "maria@email.com")
if err != nil {
t.Fatalf("erro inesperado: %v", err)
}
if u.Nome != "Maria" {
t.Errorf("nome = %q; esperado %q", u.Nome, "Maria")
}
})
t.Run("com email invalido", func(t *testing.T) {
_, err := NovoUsuario("Maria", "invalido")
if err == nil {
t.Error("esperava erro para email inválido")
}
})
})
t.Run("Validacao", func(t *testing.T) {
// mais subtestes...
})
}
Você pode executar subtestes específicos com:
go test -run TestUsuario/Criacao/com_dados_validos
Test helpers
Funções auxiliares de teste devem chamar t.Helper() para que mensagens de error apontem para a linha que chamou o helper, não para a linha dentro do helper:
func assertIgual(t *testing.T, obtido, esperado int) {
t.Helper() // fundamental para stack traces úteis
if obtido != esperado {
t.Errorf("obtido %d; esperado %d", obtido, esperado)
}
}
func assertErro(t *testing.T, err error) {
t.Helper()
if err == nil {
t.Fatal("esperava um erro, mas recebeu nil")
}
}
func assertSemErro(t *testing.T, err error) {
t.Helper()
if err != nil {
t.Fatalf("erro inesperado: %v", err)
}
}
Testes paralelos com t.Parallel
O método t.Parallel() marca um teste para execução paralela com outros testes paralelos:
func TestParalelo(t *testing.T) {
tests := []struct {
nome string
url string
}{
{"google", "https://google.com"},
{"github", "https://github.com"},
{"golang", "https://go.dev"},
}
for _, tt := range tests {
t.Run(tt.nome, func(t *testing.T) {
t.Parallel() // executa em paralelo
resp, err := http.Get(tt.url)
if err != nil {
t.Fatalf("erro ao acessar %s: %v", tt.url, err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Errorf("%s retornou status %d", tt.url, resp.StatusCode)
}
})
}
}
Atenção com a captura de variável do range em versões anteriores ao Go 1.22 — a variável do loop era compartilhada entre iterações. A partir do Go 1.22, cada iteração cria uma nova variável automaticamente.
Cobertura de testes
Go fornece análise de cobertura de código integrada ao toolchain:
# Executar testes com cobertura
go test -cover ./...
# Gerar perfil de cobertura
go test -coverprofile=coverage.out ./...
# Visualizar cobertura por função
go tool cover -func=coverage.out
# Gerar relatório HTML interativo
go tool cover -html=coverage.out -o coverage.html
O relatório HTML mostra cada linha de código colorida: verde para linhas cobertas, vermelho para não cobertas. É uma ferramenta essencial para identificar caminhos de código não testados.
Diretório testdata
O Go reserva o diretório testdata/ como um local especial para arquivos usados em testes. O go test ignora esse diretório durante o build, e ele fica acessível via caminhos relativos nos testes:
func TestProcessarJSON(t *testing.T) {
data, err := os.ReadFile("testdata/input.json")
if err != nil {
t.Fatalf("erro ao ler testdata: %v", err)
}
resultado, err := ProcessarJSON(data)
assertSemErro(t, err)
esperado, _ := os.ReadFile("testdata/expected_output.json")
if !bytes.Equal(resultado, esperado) {
t.Errorf("saída não corresponde ao esperado")
}
}
Para conteúdo embutido em testes, considere também usar embed com //go:embed testdata/*.
TestMain para setup e teardown globais
TestMain permite executar código antes e depois de todos os testes de um package:
func TestMain(m *testing.M) {
// Setup global — roda antes de todos os testes
db, err := setupTestDB()
if err != nil {
log.Fatal("falha ao configurar DB de teste:", err)
}
// Executa todos os testes
exitCode := m.Run()
// Teardown global — roda depois de todos os testes
db.Close()
cleanupTestDB()
os.Exit(exitCode)
}
TestMain é especialmente útil quando seus testes precisam de infraestrutura compartilhada como bancos de dados, servidores HTTP de teste ou arquivos temporários. Para testes de integração com containers, veja nosso tutorial sobre Testcontainers.
Boas práticas de testes em Go
Para escrever testes eficazes e manuteníveis:
- Use table-driven tests para cenários múltiplos — é o padrão idiomático do Go
- Nomeie os casos de teste descritivamente — facilita identificar falhas no CI
- Chame
t.Helper()em toda função auxiliar de teste - Prefira
t.Fatalpara pré-condições — se o setup falhar, não faz sentido continuar - Use
t.Cleanupem vez dedeferquando o cleanup precisa rodar após subtestes - Escreva benchmarks para código crítico de performance
- Mantenha testes independentes — um teste nunca deve depender do resultado de outro
- Use interfaces para mocking — injete dependências para facilitar testes unitários
- Execute com
-racepara detectar data races em código com goroutines e channels
Para um guia completo sobre testes em Go, veja nosso tutorial Testes em Go e o tutorial prático de TDD e CI/CD.
Perguntas frequentes sobre Testing em Go
Preciso de um framework de testes para Go?
Não. O package testing nativo é completo para a maioria dos projetos. Frameworks como testify adicionam assertions e mocks convenientes, mas não são necessários. Muitos projetos Go de grande escala (incluindo o próprio Go) usam apenas o package nativo. Se optar por testify, use-o para conveniência, não por necessidade.
Como faço mock de dependências em Go?
O padrão idiomático é usar interfaces. Defina uma interface para a dependência, use a implementação real em produção e uma implementação fake nos testes. Para gerar mocks automaticamente, ferramentas como mockgen (gomock) e moq são populares na comunidade.
Qual a diferença entre t.Error e t.Fatal?
t.Error registra a falha mas continua executando o teste — útil quando você quer verificar múltiplas condições. t.Fatal registra a falha e para o teste imediatamente — use para pré-condições que invalidam o resto do teste (como erros de setup). A regra é: use Fatal para condições de “não faz sentido continuar”.
Como executar apenas um teste específico?
Use a flag -run com uma expressão regular: go test -run TestNomeDoTeste ./.... Para subtestes, separe com /: go test -run TestPai/NomeDoSubteste. Para executar todos os testes de um package específico: go test ./pkg/meu-pacote/.