Golang

Testes em Go: testing, tabelas de casos e benchmarks Já leu

10 min de leitura

Testes em Go: testing, tabelas de casos e benchmarks
Em muitas linguagens, testar código exige escolher entre uma dúzia de frameworks, aprender suas APIs e configurar um ambiente separado. Em Go, testes são parte da linguagem. O pacote testing está na biblioteca padrão, o

Em muitas linguagens, testar código exige escolher entre uma dúzia de frameworks, aprender suas APIs e configurar um ambiente separado. Em Go, testes são parte da linguagem. O pacote testing está na biblioteca padrão, o comando go test está no toolchain e a convenção de nomes é simples e universalmente adotada.

Não existe uma razão legítima para escrever código Go sem testes. O custo de entrada é mínimo e os benefícios — confiança nas mudanças, documentação executável, detecção precoce de regressões — são imensos.


Anatomia de um teste

Todo arquivo de teste em Go segue três regras:

  • O nome do arquivo termina com _test.go
  • O pacote é o mesmo do código testado (ou com sufixo _test para testes externos)
  • Cada função de teste começa com Test e recebe *testing.T como único parâmetro
// arquivo: calculadora/calculadora.go
package calculadora

func Somar(a, b float64) float64 {
    return a + b
}

func Dividir(a, b float64) (float64, error) {
    if b == 0 {
        return 0, fmt.Errorf("divisão por zero")
    }
    return a / b, nil
}
// arquivo: calculadora/calculadora_test.go
package calculadora

import (
    "testing"
)

func TestSomar(t *testing.T) {
    resultado := Somar(2, 3)
    esperado := 5.0

    if resultado != esperado {
        t.Errorf("Somar(2, 3) = %.1f; esperado %.1f", resultado, esperado)
    }
}

Para executar:

go test ./...               # todos os pacotes
go test ./calculadora/      # pacote específico
go test -v ./calculadora/   # verbose — mostra cada teste
go test -run TestSomar      # executa apenas testes que correspondem ao padrão

Os métodos de *testing.T

O tipo *testing.T oferece um conjunto de métodos para reportar falhas e controlar a execução:

func TestMetodos(t *testing.T) {
    // Reporta falha mas continua o teste
    t.Error("algo deu errado")
    t.Errorf("valor obtido: %d, esperado: %d", 1, 2)

    // Reporta falha e para o teste imediatamente
    t.Fatal("falha irrecuperável")
    t.Fatalf("não foi possível abrir arquivo: %v", err)

    // Marca o teste como ignorado
    t.Skip("pulando — requer banco de dados")
    t.Skipf("pulando na versão %s", runtime.Version())

    // Log — visível apenas com -v ou quando o teste falha
    t.Log("contexto adicional")
    t.Logf("estado atual: %+v", estrutura)
}

A diferença entre Error e Fatal é importante: Error registra a falha e continua executando o restante do teste — útil para verificar múltiplas condições. Fatal para o teste imediatamente — útil quando as verificações seguintes dependem do estado atual e não fazem sentido se ele for inválido.


Tabela de casos: o padrão mais importante

O padrão de table-driven tests é o idioma central de testes em Go. Em vez de escrever uma função de teste para cada caso, define-se uma tabela com todos os casos e itera-se sobre ela:

package calculadora

import (
    "math"
    "testing"
)

func TestDividir(t *testing.T) {
    casos := []struct {
        nome      string
        a, b      float64
        esperado  float64
        erroEsperado bool
    }{
        {"divisão simples",     10,  2,  5.0,  false},
        {"divisão com decimal", 7,   2,  3.5,  false},
        {"dividendo zero",      0,   5,  0.0,  false},
        {"divisão por zero",    10,  0,  0.0,  true},
        {"negativos",          -10, -2,  5.0,  false},
        {"resultado negativo",  10, -2, -5.0,  false},
    }

    for _, tc := range casos {
        tc := tc // captura de variável de laço — necessário antes do Go 1.22
        t.Run(tc.nome, func(t *testing.T) {
            resultado, err := Dividir(tc.a, tc.b)

            if tc.erroEsperado {
                if err == nil {
                    t.Error("esperava erro, mas não recebeu nenhum")
                }
                return
            }

            if err != nil {
                t.Fatalf("erro inesperado: %v", err)
            }

            if math.Abs(resultado-tc.esperado) > 1e-9 {
                t.Errorf("Dividir(%.1f, %.1f) = %.4f; esperado %.4f",
                    tc.a, tc.b, resultado, tc.esperado)
            }
        })
    }
}

t.Run cria subtestes nomeados. Cada caso aparece separadamente na saída com -v, pode ser executado individualmente com -run TestDividir/divisão_simples e falhas em um caso não impedem os demais de executar.


Helpers de teste

Funções auxiliares reutilizadas por múltiplos testes são comuns. O método t.Helper() marca a função como helper — quando ela reporta uma falha, a linha apontada no erro é a do chamador, não a do helper:

package calculadora

import "testing"

func assertEqual(t *testing.T, obtido, esperado float64) {
    t.Helper() // essencial para mensagens de erro úteis
    if obtido != esperado {
        t.Errorf("obtido %.4f; esperado %.4f", obtido, esperado)
    }
}

func assertErro(t *testing.T, err error) {
    t.Helper()
    if err == nil {
        t.Error("esperava um erro, mas não recebeu nenhum")
    }
}

func assertSemErro(t *testing.T, err error) {
    t.Helper()
    if err != nil {
        t.Fatalf("erro inesperado: %v", err)
    }
}

func TestSomarComHelpers(t *testing.T) {
    assertEqual(t, Somar(2, 3), 5.0)
    assertEqual(t, Somar(-1, 1), 0.0)
    assertEqual(t, Somar(0, 0), 0.0)
}

Setup e Teardown com TestMain

Quando testes precisam de setup global — como inicializar um banco de dados de teste ou criar arquivos temporários — TestMain permite executar código antes e depois de todos os testes do pacote:

package calculadora

import (
    "fmt"
    "os"
    "testing"
)

func TestMain(m *testing.M) {
    // Setup — executado antes de todos os testes
    fmt.Println("Inicializando ambiente de teste...")

    // m.Run() executa todos os testes e retorna o código de saída
    codigo := m.Run()

    // Teardown — executado após todos os testes
    fmt.Println("Limpando ambiente de teste...")

    os.Exit(codigo)
}

Para setup e teardown por teste individual, use funções auxiliares chamadas no início e defer para limpeza:

func configurarTeste(t *testing.T) func() {
    t.Helper()
    // setup
    dir, err := os.MkdirTemp("", "teste-*")
    if err != nil {
        t.Fatal(err)
    }

    // retorna função de teardown
    return func() {
        os.RemoveAll(dir)
    }
}

func TestComArquivos(t *testing.T) {
    teardown := configurarTeste(t)
    defer teardown()

    // ... teste usando os arquivos temporários
}

Benchmarks: medindo performance

Funções de benchmark seguem a mesma convenção dos testes, com Benchmark no lugar de Test e *testing.B como parâmetro:

package calculadora

import "testing"

func BenchmarkSomar(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Somar(42.0, 58.0)
    }
}

func BenchmarkDividir(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Dividir(100.0, 3.0)
    }
}

b.N é ajustado automaticamente pelo framework até que o resultado seja estatisticamente confiável. Para executar benchmarks:

go test -bench=.                    # todos os benchmarks
go test -bench=BenchmarkSomar       # benchmark específico
go test -bench=. -benchmem          # inclui alocações de memória
go test -bench=. -count=5           # executa 5 vezes para estabilidade
go test -bench=. -benchtime=3s      # executa por 3 segundos em vez do padrão

Saída típica:

BenchmarkSomar-8      1000000000     0.3125 ns/op
BenchmarkDividir-8    756123456      1.584  ns/op     0 B/op    0 allocs/op

Os campos significam: nome do benchmark, número de iterações, tempo por operação, bytes alocados por operação e número de alocações por operação.


Benchmarks com reset e pause

Quando o setup do benchmark é custoso e não deve ser medido, b.ResetTimer zera o timer após o setup:

func BenchmarkProcessarSliceGrande(b *testing.B) {
    // Setup custoso — não deve ser medido
    dados := make([]int, 100000)
    for i := range dados {
        dados[i] = i
    }

    b.ResetTimer() // começa a medir aqui

    for i := 0; i < b.N; i++ {
        processarSlice(dados)
    }
}

Para pausar e retomar o timer durante o loop:

func BenchmarkComIO(b *testing.B) {
    for i := 0; i < b.N; i++ {
        b.StopTimer()
        // preparar dados para esta iteração
        dados := gerarDados()
        b.StartTimer()

        processar(dados)
    }
}

Testes de exemplo: documentação executável

Funções de exemplo começam com Example e servem simultaneamente como documentação e como testes. O comentário // Output: define a saída esperada — se não corresponder, o teste falha:

func ExampleSomar() {
    resultado := Somar(3, 4)
    fmt.Println(resultado)
    // Output:
    // 7
}

func ExampleDividir() {
    resultado, err := Dividir(10, 4)
    if err != nil {
        fmt.Println("Erro:", err)
        return
    }
    fmt.Printf("%.2f\n", resultado)
    // Output:
    // 2.50
}

Esses exemplos aparecem automaticamente na documentação gerada pelo go doc e pelo pkg.go.dev, tornando-os a forma mais eficiente de documentar comportamento — a documentação nunca fica desatualizada porque é verificada pelo go test.


Cobertura de testes

Go possui suporte nativo para medir cobertura de código:

# Executar testes com cobertura
go test -cover ./...

# Gerar arquivo de cobertura detalhado
go test -coverprofile=cobertura.out ./...

# Visualizar no terminal
go tool cover -func=cobertura.out

# Visualizar em HTML — abre no navegador
go tool cover -html=cobertura.out

A visualização HTML colore cada linha: verde para linhas executadas durante os testes, vermelho para linhas não cobertas. É a forma mais rápida de identificar cenários não testados.


Testes paralelos

Testes que não compartilham estado podem ser executados em paralelo para reduzir o tempo total:

func TestOperacoesParalelas(t *testing.T) {
    casos := []struct {
        nome string
        a, b float64
        esp  float64
    }{
        {"caso1", 1, 2, 3},
        {"caso2", 4, 5, 9},
        {"caso3", 10, 20, 30},
    }

    for _, tc := range casos {
        tc := tc
        t.Run(tc.nome, func(t *testing.T) {
            t.Parallel() // este subteste pode rodar em paralelo com outros
            resultado := Somar(tc.a, tc.b)
            if resultado != tc.esp {
                t.Errorf("obtido %.1f, esperado %.1f", resultado, tc.esp)
            }
        })
    }
}
go test -parallel 4 ./...  # até 4 testes em paralelo

O detector de race conditions

Go possui um detector de race conditions integrado que instrumenta o binário para detectar acessos concorrentes não sincronizados:

go test -race ./...
go run -race main.go
go build -race -o app-com-race ./

O detector tem custo em tempo de execução — cerca de 5 a 10 vezes mais lento. Use em CI/CD e em desenvolvimento, mas não em binários de produção.


Resumo do que foi coberto

Este artigo apresentou o sistema de testes do Go em profundidade: a anatomia de um arquivo de teste, os métodos de *testing.T, o padrão de tabela de casos com subtestes, helpers com t.Helper(), setup e teardown com TestMain, benchmarks com controle de timer, testes de exemplo como documentação executável, medição de cobertura, testes paralelos e o detector de race conditions. Com esse módulo completo, o curso avança para o tema mais característico do Go: concorrência.


Referências e leituras complementares

  • Documentação do pacote testing — Referência completa de T, B, F e todos os métodos. https://pkg.go.dev/testing

  • Go Blog: The Go test command — Visão geral do sistema de testes. https://go.dev/doc/code#Testing

  • Go Blog: Table Driven Tests — Artigo de Dave Cheney sobre o padrão de tabela de casos. https://dave.cheney.net/2019/05/07/prefer-table-driven-tests

  • Go by Example: Testing — Exemplos práticos comentados de testes em Go. https://gobyexample.com/testing

  • Go by Example: Benchmarks — Exemplos práticos de benchmarks. https://gobyexample.com/benchmarks

  • Documentação do go test — Todas as flags disponíveis para o comando go test. https://pkg.go.dev/cmd/go#hdr-Test_packages

Comentários

Mais em Golang

Go com MongoDB: operações CRUD com o driver oficial
Go com MongoDB: operações CRUD com o driver oficial

MongoDB armazena dados como documentos BSON — uma representação binária de JS...

Artigo 40 — Variáveis de ambiente e configuração com Viper
Artigo 40 — Variáveis de ambiente e configuração com Viper

Artigo 40 — Variáveis de ambiente e configuração com Viper Configuração: o el...

Pacotes: visibilidade, nomenclatura e organização de projeto
Pacotes: visibilidade, nomenclatura e organização de projeto

Em Go, todo arquivo fonte pertence a um pacote. Não existe código solto fora...