Testes
Lição 23: Testes
Analogia da Vida
Imagine que você abre um restaurante. Antes de cada prato ser servido, o chef prova primeiro — para confirmar que o tempero está certo e o cozimento foi feito corretamente. Testes de software são como esse processo de "provar": antes que o código seja entregue aos usuários, ele é verificado automaticamente para garantir que funciona conforme o esperado. Ir para produção sem testes é como servir comida não provada aos clientes — mais cedo ou mais tarde, problemas surgirão.
Conceitos Centrais
Go possui uma cadeia de ferramentas de teste completa integrada, sem necessidade de frameworks de terceiros. Os conceitos centrais são:
| Conceito | Descrição |
|---|---|
testing.T |
Objeto de contexto de teste unitário, usado para reportar falhas de teste |
testing.B |
Objeto de contexto de teste de benchmark, usado para medir desempenho |
testing.M |
Objeto de entrada de teste, usado para configuração global com TestMain |
*_test.go |
Convenção de nomenclatura de arquivo de teste, compilado apenas durante testes |
go test |
Ferramenta de linha de comando para executar testes |
| Testes orientados por tabela | Padrão idiomático para conduzir múltiplos casos de teste com uma tabela de dados |
httptest |
Pacote auxiliar de teste para requisições HTTP |
Sintaxe Básica e Uso
Convenções de Arquivo de Teste
Arquivos de teste devem terminar com _test.go e ser colocados no mesmo pacote do código sendo testado:
myapp/
├── math.go
└── math_test.go
Assinatura da Função de Teste
func TestXxx(t *testing.T) {
// Lógica do teste
}
O nome da função deve começar com Test, e o parâmetro é *testing.T.
Métodos de Asserção
Go não possui função de asserção integrada — você precisa verificar manualmente:
func TestAdd(t *testing.T) {
result := Add(2, 3)
if result != 5 {
t.Errorf("Add(2, 3) = %d, esperado 5", result)
}
}
Métodos comuns:
| Método | Propósito |
|---|---|
t.Error() / t.Errorf() |
Reportar erro, continuar execução |
t.Fatal() / t.Fatalf() |
Reportar erro, terminar imediatamente o teste atual |
t.Skip() / t.Skipf() |
Pular o teste atual |
t.Log() / t.Logf() |
Saída de log (mostrado apenas com -v) |
t.Run(name, func) |
Executar subteste |
t.Helper() |
Marcar como função auxiliar, erros apontam para o chamador |
💡 Dica 2: Use t.Helper() para marcar suas funções auxiliares de asserção, para que falhas reportem a localização do chamador em vez de dentro da função auxiliar.
💡 Dica 3: go test -v mostra saída detalhada, go test -run=regex executa apenas testes correspondentes, -count=1 desabilita cache.
Executando Testes
# Executar todos os testes no pacote atual
go test
# Saída detalhada
go test -v
# Executar testes que correspondem a um nome
go test -run TestAdd
# Executar no diretório atual e subdiretórios
go test ./...
# Mostrar cobertura
go test -cover
Exemplos
Exemplo: Testes Unitários Básicos (Dificuldade ⭐)
Estrutura de arquivos:
calculator/
├── calc.go
└── calc_test.go
calc.go:
package calculator
// Add retorna a soma de dois números
func Add(a, b int) int {
return a + b
}
// Divide retorna o quociente e se há erro
func Divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("divisor não pode ser zero")
}
return a / b, nil
}
calc_test.go:
package calculator
import (
"testing"
)
func TestAdd(t *testing.T) {
got := Add(2, 3)
want := 5
if got != want {
t.Errorf("Add(2, 3) = %d, esperado %d", got, want)
}
}
func TestDivide(t *testing.T) {
got, err := Divide(10, 3)
if err != nil {
t.Fatalf("Erro inesperado: %v", err)
}
want := 3.3333333333333335
if got != want {
t.Errorf("Divide(10, 3) = %f, esperado %f", got, want)
}
}
func TestDivideByZero(t *testing.T) {
_, err := Divide(10, 0)
if err == nil {
t.Error("Divisão por zero deveria retornar um erro")
}
}
Saída:
$ go test -v
=== RUN TestAdd
--- PASS: TestAdd (0.00s)
=== RUN TestDivide
--- PASS: TestDivide (0.00s)
=== RUN TestDivideByZero
--- PASS: TestDivideByZero (0.00s)
PASS
ok calculator 0.003s
Exemplo: Testes Orientados por Tabela e Subtestes (Dificuldade ⭐⭐)
Testes orientados por tabela são o padrão de teste mais elogiado na comunidade Go — coloque entradas e saídas esperadas em um slice, depois faça loop e verifique.
stringutil.go:
package stringutil
import "unicode"
// Reverse inverte uma string
func Reverse(s string) string {
runes := []rune(s)
for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
runes[i], runes[j] = runes[j], runes[i]
}
return string(runes)
}
// IsPalindrome verifica se uma string é um palíndromo
func IsPalindrome(s string) bool {
runes := []rune(s)
for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
if unicode.ToLower(runes[i]) != unicode.ToLower(runes[j]) {
return false
}
}
return true
}
stringutil_test.go:
package stringutil
import "testing"
func TestReverse(t *testing.T) {
tests := []struct {
name string
input string
want string
}{
{"string vazia", "", ""},
{"caractere único", "a", "a"},
{"string normal", "hello", "olleh"},
{"chinês", "你好世界", "界世好你"},
{"palíndromo", "racecar", "racecar"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Reverse(tt.input)
if got != tt.want {
t.Errorf("Reverse(%q) = %q, esperado %q", tt.input, got, tt.want)
}
})
}
}
func TestIsPalindrome(t *testing.T) {
tests := []struct {
name string
input string
want bool
}{
{"palíndromo em inglês", "racecar", true},
{"palíndromo em chinês", "上海自来水来自海上", true},
{"não é palíndromo", "hello", false},
{"case insensitive", "RaceCar", true},
{"string vazia", "", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := IsPalindrome(tt.input)
if got != tt.want {
t.Errorf("IsPalindrome(%q) = %v, esperado %v", tt.input, got, tt.want)
}
})
}
}
Executando um subteste específico:
$ go test -v -run=TestIsPalindrome/Chinese_palindrome
=== RUN TestIsPalindrome/Chinese_palindrome
--- PASS: TestIsPalindrome/Chinese_palindrome (0.00s)
PASS
Exemplo: Benchmark, TestMain e httptest (Dificuldade ⭐⭐⭐)
handler.go:
package handler
import (
"encoding/json"
"net/http"
)
type Response struct {
Message string `json:"message"`
Code int `json:"code"`
}
// HelloHandler lida com requisições /hello
func HelloHandler(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
if name == "" {
name = "World"
}
resp := Response{
Message: "Hello, " + name,
Code: 200,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}
handler_test.go:
package handler
import (
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"testing"
)
// TestMain é executado antes de todos os testes
func TestMain(m *testing.M) {
setup()
code := m.Run()
teardown()
os.Exit(code)
}
func setup() {
// Código de inicialização
}
func teardown() {
// Código de limpeza
}
func TestHelloHandler(t *testing.T) {
tests := []struct {
name string
queryParam string
wantMsg string
wantCode int
}{
{"nome padrão", "", "Hello, World", 200},
{"nome personalizado", "Go", "Hello, Go", 200},
{"nome em chinês", "小明", "Hello, 小明", 200},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
url := "/hello"
if tt.queryParam != "" {
url += "?name=" + tt.queryParam
}
req := httptest.NewRequest(http.MethodGet, url, nil)
rr := httptest.NewRecorder()
HelloHandler(rr, req)
if rr.Code != tt.wantCode {
t.Errorf("Código de status = %d, esperado %d", rr.Code, tt.wantCode)
}
var resp Response
if err := json.NewDecoder(rr.Body).Decode(&resp); err != nil {
t.Fatalf("Falha ao analisar resposta: %v", err)
}
if resp.Message != tt.wantMsg {
t.Errorf("Mensagem = %q, esperado %q", resp.Message, tt.wantMsg)
}
})
}
}
// BenchmarkHelloHandler teste de benchmark
func BenchmarkHelloHandler(b *testing.B) {
req := httptest.NewRequest(http.MethodGet, "/hello?name=Go", nil)
for i := 0; i < b.N; i++ {
rr := httptest.NewRecorder()
HelloHandler(rr, req)
}
}
Executando benchmarks:
$ go test -bench=. -benchmem -count=3
BenchmarkHelloHandler-8 200000 7523 ns/op 1248 B/op 18 allocs/op
PASS
ok handler 4.832s
Visualizando cobertura:
$ go test -coverprofile=coverage.out
$ go tool cover -func=coverage.out
total: (statements) 85.7%
$ go tool cover -html=coverage.out -o coverage.html
Cenários de Aplicação
Cenário 1: Testando Middleware
package middleware
import (
"net/http"
"strings"
)
// AuthMiddleware verifica Token nos cabeçalhos da requisição
func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if !strings.HasPrefix(token, "Bearer ") {
http.Error(w, "Não autorizado", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}
Testando:
package middleware
import (
"net/http"
"net/http/httptest"
"testing"
)
func TestAuthMiddleware(t *testing.T) {
nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("Aprovado"))
})
middleware := AuthMiddleware(nextHandler)
t.Run("sem token deve retornar 401", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/api/data", nil)
rr := httptest.NewRecorder()
middleware.ServeHTTP(rr, req)
if rr.Code != http.StatusUnauthorized {
t.Errorf("Código de status = %d, esperado %d", rr.Code, http.StatusUnauthorized)
}
})
t.Run("token válido deve passar", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/api/data", nil)
req.Header.Set("Authorization", "Bearer my-secret-token")
rr := httptest.NewRecorder()
middleware.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Errorf("Código de status = %d, esperado %d", rr.Code, http.StatusOK)
}
})
}
Cenário 2: Testando Operações de Banco de Dados (Isolamento por Interface)
package user
import "context"
// UserRepository define a interface de acesso a dados do usuário
type UserRepository interface {
GetByID(ctx context.Context, id int) (*User, error)
Create(ctx context.Context, user *User) error
}
type User struct {
ID int
Name string
}
type Service struct {
repo UserRepository
}
func NewService(repo UserRepository) *Service {
return &Service{repo: repo}
}
func (s *Service) GetUser(ctx context.Context, id int) (*User, error) {
if id <= 0 {
return nil, fmt.Errorf("ID de usuário inválido: %d", id)
}
return s.repo.GetByID(ctx, id)
}
Mock e Teste:
package user
import (
"context"
"testing"
)
type mockRepo struct {
users map[int]*User
}
func (m *mockRepo) GetByID(_ context.Context, id int) (*User, error) {
if u, ok := m.users[id]; ok {
return u, nil
}
return nil, fmt.Errorf("usuário %d não existe", id)
}
func (m *mockRepo) Create(_ context.Context, user *User) error {
m.users[user.ID] = user
return nil
}
func TestGetUser(t *testing.T) {
mock := &mockRepo{
users: map[int]*User{
1: {ID: 1, Name: "Alice"},
2: {ID: 2, Name: "Bob"},
},
}
svc := NewService(mock)
t.Run("usuário existente", func(t *testing.T) {
user, err := svc.GetUser(context.Background(), 1)
if err != nil {
t.Fatalf("Erro inesperado: %v", err)
}
if user.Name != "Alice" {
t.Errorf("Nome = %q, esperado %q", user.Name, "Alice")
}
})
t.Run("usuário inexistente", func(t *testing.T) {
_, err := svc.GetUser(context.Background(), 99)
if err == nil {
t.Error("Esperava que um erro fosse retornado")
}
})
}
❓ Perguntas Frequentes
P1: E se os arquivos de teste não forem reconhecidos?
Certifique-se de que o nome do arquivo termina com _test.go e o nome do pacote está correto. Arquivos de teste são ignorados durante go build e apenas compilados durante go test.
P2: Como executar apenas testes que falharam?
go test -run TestDivideByZero -v
go test -run TestSomething -count=1 -v
P3: Qual a diferença entre t.Fatal e t.Error?
t.Error()/t.Errorf(): Reporta erro, continua executando o código restante.t.Fatal()/t.Fatalf(): Reporta erro, termina imediatamente a função de teste.
Princípio geral: Se asserções subsequentes dependem de resultados anteriores, use Fatal; para múltiplas asserções independentes, use Error.
P4: Como pular testes que consomem tempo?
func TestSlowOperation(t *testing.T) {
if testing.Short() {
t.Skip("Pulando teste que consome tempo (use a flag -short)")
}
// Operação que consome tempo...
}
go test -short
📖 Resumo
- Funções de Teste: Começam com
Test, parâmetro*testing.T, useError/Fatalpara reportar falhas - Testes Orientados por Tabela: Definir casos com um slice, loop +
t.Runpara executar - Subtestes:
t.Run(name, func)permite que casos sejam executados independentemente - Testes de Benchmark: Começam com
Benchmark, parâmetro*testing.B, loopb.Nvezes - TestMain: Ponto de entrada global para setup/teardown, chama
m.Run() - httptest:
httptest.NewRequest+httptest.NewRecorderpara testar handlers HTTP - Cobertura:
go test -coverpara visualizar,-coverprofilepara gerar relatórios
📝 Exercícios
Exercício 1: Escrever Testes para Funções de Processamento de String
Escreva testes para a seguinte função, contendo pelo menos 5 casos orientados por tabela:
// Truncate trunca uma string para o comprimento especificado
func Truncate(s string, maxLen int) string {
runes := []rune(s)
if len(runes) <= maxLen {
return s
}
return string(runes[:maxLen-3]) + "..."
}
Exercício 2: Escrever Benchmarks para Handlers HTTP
Escreva benchmarks comparando json.Marshal e json.NewEncoder:
func ListHandler(w http.ResponseWriter, r *http.Request) {
users := []User{{1, "Alice"}, {2, "Bob"}, {3, "Charlie"}}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(users)
}
Exercício 3: Usar TestMain para Implementar Banco de Dados de Teste
- Criar um banco de dados SQLite temporário em
TestMain - Executar migrações para criar tabelas
- Executar todos os testes
- Excluir o banco de dados temporário após os testes



