Testes unitários em Go com o pacote testing
Como escrever testes com o pacote testing do Go: convenções de arquivos, caixa-preta e caixa-branca, e a diferença entre Errorf e Fatalf.
Publicada em · 6 min de leitura
Diferente da maioria das linguagens de programação modernas — que dependem de frameworks externos de terceiros (como JUnit no Java, Jest/Mocha no Node.js ou PyTest no Python) —, o Go nasceu com uma suíte de testes completa, robusta e ultra-rápida embutida diretamente na ferramenta oficial do compilador (go test).
Os criadores do Go projetaram o sistema de testes para que testes fossem código Go comum e idiomático: sem anotações mágicas, sem reflexão oculta e sem DSLs complexas. Se você sabe programar em Go, você já sabe escrever testes em Go.
Esta aula ensina a mecânica fundamental do pacote nativo testing, as convenções de arquivos e pacotes, a distinção entre testes de caixa-preta e caixa-branca, e como medir a cobertura de código com relatórios visuais no navegador.
O Mapa da Aula
| Parte | Seção | O que você vai dominar |
|---|---|---|
| 1. Fundamentos | 1 e 2 | As regras sagradas de arquivos _test.go, assinaturas de funções e exclusão em produção |
| 2. Tipos de Teste | 3 | Pacote interno (package foo) vs pacote externo (package foo_test - caixa-preta) |
| 3. Mecânica do testing.T | 4 e 5 | t.Errorf, t.Fatalf, t.Logf e o papel crucial de t.Helper() em funções utilitárias |
| 4. Comandos e Flags | 6 | Executando testes com -v, filtrando com -run e ativando o race detector com -race |
| 5. Cobertura de Código | 7 e 8 | Gerando coverprofile e inspecionando linhas cobertas em HTML interativo |
Parte 1: Fundamentos e Convenções do go test
1. As 3 Regras Sagradas
Para que o compilador do Go reconheça e execute seus testes unitários, você só precisa seguir três convenções canônicas:
- Nome do Arquivo: O arquivo de teste deve obrigatoriamente terminar com o sufixo
_test.go(por exemplo: se o seu código está emcalculadora.go, o teste deve ficar emcalculadora_test.gono mesmo diretório). - Nome da Função: A função de teste deve iniciar com a palavra
Testseguida por uma letra maiúscula (ex:TestSoma,TestValidarEmail). Funções comotestSoma(com ‘t’ minúsculo) são ignoradas pelo runner. - Assinatura da Função: A função deve receber exatamente um parâmetro do tipo ponteiro para
testing.T:func TestNomeDoCenario(t *testing.T) { // lógica de teste }
Isolamento no Binário de Produção
Quando você executa go build para compilar sua aplicação para produção, o compilador do Go ignora completamente todos os arquivos terminados em _test.go. Eles nunca entram no executável final de produção, garantindo binários leves e livres de código de teste.
Parte 2: Pacote Interno vs Pacote Externo (Caixa-Preta)
No topo do seu arquivo _test.go, você tem duas opções de declaração de pacote:
1. Testes de Caixa-Branca (package calculadora)
Se você declarar o mesmo nome do pacote sob teste, seu teste terá acesso a funções e structs privadas (não exportadas) iniciadas com letra minúscula. É útil quando você precisa testar um algoritmo matemático interno complexo sem expô-lo para o público.
2. Testes de Caixa-Preta (package calculadora_test) — Recomendado
Ao adicionar o sufixo _test ao nome do pacote, o Go trata o teste como se ele fosse um consumidor externo do seu módulo. Você é obrigado a importar o pacote (import "meu-modulo/calculadora") e só consegue acessar métodos públicos (iniciados com letra maiúscula).
Parte 3: A Mecânica das Asserções Nativas
Por que Go não tem assert.Equal nativo na biblioteca padrão? Os criadores do Go explicaram que asserções com if comum forçam mensagens de erro mais ricas e descritivas:
func TestDividir(t *testing.T) {
got, err := Dividir(10, 2)
if err != nil {
t.Fatalf("Dividir(10, 2) retornou erro inesperado: %v", err)
}
if got != 5 {
t.Errorf("Dividir(10, 2) = %d; esperado = 5", got)
}
}
4. t.Errorf vs t.Fatalf
t.Errorf(Falha Não-Crítica): Registra que o teste falhou e imprime a mensagem formatada no terminal, mas continua a execução das próximas linhas do teste. Use para validar valores divergentes onde você quer ver todos os campos com erro de uma vez.t.Fatalf(Falha Fatal): Registra a falha e aborta a execução do teste imediatamente chamandot.FailNow(). Use para pré-condições onde continuar causaria um panic (ex: seerr != nil, você não deve tentar ler o objeto de retorno).
5. O papel de t.Helper() em funções auxiliares
Quando criamos funções utilitárias para evitar duplicação em testes (por exemplo, uma função que cria um usuário de teste), se uma asserção falhar dentro dela, o Go reportará a linha interna da função auxiliar, dificultando saber qual teste específico falhou.
Ao declarar t.Helper() na primeira linha da sua função auxiliar, o runtime do Go remove esse frame do stack trace e reporta a linha exata do teste chamador:
func assertStringIgual(t *testing.T, esperado, obtido string) {
t.Helper() // Avisa o compilador: reporte o erro na linha de quem me chamou!
if obtido != esperado {
t.Errorf("esperava %q, recebeu %q", esperado, obtido)
}
}
Parte 4: Comandos Essenciais do go test
- Executar todos os testes do projeto recursivamente:
go test ./... - Modo Verboso (
-v): Exibe o nome de cada função executada, o status (RUN,PASS,FAIL) e as mensagens emitidas port.Logf:go test -v ./... - Executar apenas testes específicos (
-run): Aceita uma expressão regular para filtrar pelo nome da função:# Executa apenas testes que tenham "Email" no nome go test -v -run Email ./... - Verificação de Condições de Corrida (
-race): Compila o código com instrumentação do ThreadSanitizer e detecta qualquer acesso concorrente inseguro:go test -race ./...
Parte 5: Medindo Cobertura de Código
Saber quais linhas do seu código foram exercitadas pelos testes é fundamental para encontrar caminhos esquecidos:
1. Medição no terminal com -cover:
$ go test -cover ./...
ok meu-modulo/calculadora 0.012s coverage: 87.5% of statements
2. Gerando relatório detalhado de cobertura:
# Grava as estatísticas linha a linha no arquivo coverage.out
go test -coverprofile=coverage.out ./...
3. Visualização interativa no navegador:
# Abre o navegador exibindo o código-fonte em verde (coberto) e vermelho (não coberto)
go tool cover -html=coverage.out
Parte 6: Código Prático Completo
Código de Negócio (validador.go):
package validador
import (
"errors"
"strings"
)
var (
ErrEmailVazio = errors.New("o e-mail não pode ser vazio")
ErrDominioInvalido = errors.New("o domínio do e-mail é inválido")
)
// ValidarEmailCorporativo verifica se o e-mail possui formato e domínio válidos.
func ValidarEmailCorporativo(email, dominioEsperado string) (bool, error) {
emailLimpo := strings.TrimSpace(email)
if emailLimpo == "" {
return false, ErrEmailVazio
}
partes := strings.Split(emailLimpo, "@")
if len(partes) != 2 || partes[0] == "" || partes[1] == "" {
return false, errors.New("formato de e-mail malformado")
}
if partes[1] != dominioEsperado {
return false, ErrDominioInvalido
}
return true, nil
}
Código de Teste (validador_test.go):
package validador_test
import (
"errors"
"testing"
"meu-modulo/validador"
)
// Função auxiliar com t.Helper()
func assertErroContem(t *testing.T, err error, esperado error) {
t.Helper()
if !errors.Is(err, esperado) {
t.Fatalf("erro recebido = %v; esperava conter = %v", err, esperado)
}
}
func TestValidarEmailCorporativo_Sucesso(t *testing.T) {
valido, err := validador.ValidarEmailCorporativo("dev@empresa.com", "empresa.com")
if err != nil {
t.Fatalf("esperava sucesso, recebeu erro: %v", err)
}
if !valido {
t.Errorf("esperava email válido, recebeu falso")
}
}
func TestValidarEmailCorporativo_EmailVazio(t *testing.T) {
valido, err := validador.ValidarEmailCorporativo(" ", "empresa.com")
if err == nil {
t.Fatalf("esperava erro para email vazio, mas retornou nil")
}
if valido {
t.Errorf("esperava que valido fosse falso")
}
assertErroContem(t, err, validador.ErrEmailVazio)
}
func TestValidarEmailCorporativo_DominioIncorreto(t *testing.T) {
valido, err := validador.ValidarEmailCorporativo("dev@gmail.com", "empresa.com")
if err == nil {
t.Fatalf("esperava erro para domínio divergente, retornou nil")
}
if valido {
t.Errorf("esperava que valido fosse falso")
}
assertErroContem(t, err, validador.ErrDominioInvalido)
}