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

GO
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:

GO
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 1: Parâmetros e valores de retorno de funções de teste devem ser o mais simples possível para facilitar a verificação. Use testes orientados por tabela para entradas complexas.

💡 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

BASH
# 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
▶ Experimente

calc.go:

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:

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:

BASH
$ 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:

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
}
▶ Experimente

stringutil_test.go:

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:

BASH
$ go test -v -run=TestIsPalindrome/Chinese_palindrome
=== RUN   TestIsPalindrome/Chinese_palindrome
--- PASS: TestIsPalindrome/Chinese_palindrome (0.00s)
PASS
💡 O benefício dos testes orientados por tabela: adicionar um novo caso requer apenas adicionar uma linha ao slice, sem necessidade de escrever uma nova função de teste.


Exemplo: Benchmark, TestMain e httptest (Dificuldade ⭐⭐⭐)

handler.go:

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)
}
▶ Experimente

handler_test.go:

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:

BASH
$ 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:

BASH
$ 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

GO
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:

GO
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)

GO
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:

GO
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?

BASH
go test -run TestDivideByZero -v
go test -run TestSomething -count=1 -v

P3: Qual a diferença entre t.Fatal e t.Error?

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?

GO
func TestSlowOperation(t *testing.T) {
    if testing.Short() {
        t.Skip("Pulando teste que consome tempo (use a flag -short)")
    }
    // Operação que consome tempo...
}
BASH
go test -short

📖 Resumo

  1. Funções de Teste: Começam com Test, parâmetro *testing.T, use Error/Fatal para reportar falhas
  2. Testes Orientados por Tabela: Definir casos com um slice, loop + t.Run para executar
  3. Subtestes: t.Run(name, func) permite que casos sejam executados independentemente
  4. Testes de Benchmark: Começam com Benchmark, parâmetro *testing.B, loop b.N vezes
  5. TestMain: Ponto de entrada global para setup/teardown, chama m.Run()
  6. httptest: httptest.NewRequest + httptest.NewRecorder para testar handlers HTTP
  7. Cobertura: go test -cover para visualizar, -coverprofile para 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:

GO
// 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:

GO
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

  1. Criar um banco de dados SQLite temporário em TestMain
  2. Executar migrações para criar tabelas
  3. Executar todos os testes
  4. Excluir o banco de dados temporário após os testes

Próxima Lição

Próxima Lição: Expressões Regulares e Data →

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%