Testes em Go
Nesta aula, você aprenderá a criar testes unitários em Go usando o pacote testing, a convenção func TestXxx, o comando go test e a técnica de table-driven tests. Vamos explorar desde os conceitos básicos até padrões mais avançados, com exemplos práticos e exercícios para fixar o conteúdo.
Testes são uma parte fundamental do desenvolvimento de software, pois garantem que o código funcione como esperado e facilitam mudanças futuras. Em Go, a linguagem foi projetada com testes em mente, oferecendo ferramentas nativas e uma abordagem simples e eficiente. Nesta aula, você aprenderá a escrever testes unitários usando o pacote testing, a convenção func TestXxx, o comando go test e a técnica de table-driven tests, que é amplamente adotada na comunidade Go.
Vamos começar com os fundamentos e, aos poucos, avançar para padrões mais sofisticados, sempre com exemplos práticos e explicações detalhadas. Ao final, você terá uma base sólida para testar seus próprios programas em Go.
Pacote testing
O pacote testing é parte da biblioteca padrão de Go e fornece suporte para testes automatizados. Ele define tipos e funções que permitem escrever testes unitários, benchmarks e exemplos. O tipo mais importante é *testing.T, usado para controlar testes de unidade. Com ele, você pode relatar falhas, pular testes, registrar logs e muito mais.
Para usar o pacote, basta importá-lo em um arquivo de teste. Convencionalmente, arquivos de teste terminam com _test.go, e o Go os reconhece automaticamente quando você executa go test. O pacote testing também oferece funções como t.Errorf, t.Fatalf, t.Logf, t.Skip, entre outras.
Exemplo básico de um arquivo de teste:
package main
import (
"testing"
)
func TestSoma(t *testing.T) {
resultado := soma(2, 3)
if resultado != 5 {
t.Errorf("soma(2, 3) = %d; esperado 5", resultado)
}
}
Note que a função de teste recebe um parâmetro *testing.T, que é usado para relatar falhas. O método Errorf registra uma falha, mas permite que o teste continue; já Fatalf interrompe o teste imediatamente.
func TestXxx
Em Go, funções de teste seguem a convenção func TestXxx(t *testing.T), onde Xxx é um nome descritivo que começa com letra maiúscula. O nome deve indicar o que está sendo testado. Por exemplo, TestSoma, TestDivide, TestParseJSON. Essa convenção é reconhecida pelo comando go test, que executa todas as funções com esse padrão.
Além de testes de unidade, você pode definir funções com nomes BenchmarkXxx para benchmarks e ExampleXxx para exemplos (que também funcionam como testes). O prefixo Test é reservado para testes, e a função deve ter exatamente um parâmetro do tipo *testing.T.
Exemplo de múltiplos testes:
package main
import "testing"
func TestSoma(t *testing.T) {
if soma(1, 2) != 3 {
t.Error("soma(1,2) != 3")
}
}
func TestSubtracao(t *testing.T) {
if subtracao(5, 3) != 2 {
t.Error("subtracao(5,3) != 2")
}
}
É importante que as funções de teste sejam independentes e não dependam de ordem de execução. Cada teste deve verificar uma unidade de código isolada.
go test
O comando go test é a ferramenta principal para executar testes em Go. Ele compila o pacote atual e executa todas as funções de teste encontradas nos arquivos _test.go. Por padrão, ele roda os testes do diretório atual, mas você pode especificar pacotes ou usar flags para controlar o comportamento.
Algumas flags úteis:
-v: exibe detalhes de cada teste executado (verbose).-run: filtra testes por padrão de nome, por exemplo,-run TestSoma.-count: repete testes, útil para detectar flakiness.-cover: mostra a cobertura de código (porcentagem de linhas executadas pelos testes).
Exemplo de execução:
go test -v ./...
Esse comando executa os testes de todos os pacotes do módulo, exibindo detalhes. O go test também gera um cache de resultados, acelerando execuções subsequentes.
Além de testes, o go test pode executar benchmarks e exemplos. Benchmarks são funções com prefixo Benchmark e são executados com a flag -bench.
Table-driven tests
Table-driven tests são uma técnica onde você define uma tabela (slice de structs) com casos de teste, cada um contendo entradas e saídas esperadas. Em seguida, você itera sobre a tabela, executando o mesmo código de teste para cada caso. Isso reduz duplicação e torna os testes mais fáceis de expandir.
O padrão é simples: defina uma struct para o caso de teste, crie uma slice com todos os casos e use um loop com t.Run para subtests. t.Run permite executar subtestes nomeados, o que melhora a saída e permite executar um subteste específico.
Exemplo de table-driven test para a função soma:
package main
import "testing"
func TestSoma(t *testing.T) {
testes := []struct {
nome string
a, b int
esperado int
}{
{"positivos", 1, 2, 3},
{"negativos", -1, -2, -3},
{"zero", 0, 0, 0},
}
for _, tt := range testes {
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)
}
})
}
}
Essa abordagem é amplamente utilizada em projetos Go porque facilita a adição de novos casos e torna o teste mais legível. Além disso, com -run TestSoma/positivos você pode executar um caso específico.
Boas práticas e observações
Ao escrever testes em Go, lembre-se de:
- Manter os testes independentes e determinísticos.
- Usar nomes descritivos para funções de teste e subtests.
- Testar casos de borda, como entradas vazias, zero, valores negativos, etc.
- Usar
t.Cleanuppara liberar recursos, se necessário. - Executar
go test -coverpara verificar a cobertura e identificar áreas não testadas.
Referências
- Documentação oficial do pacote testing
- Como escrever código Go - Testes
- Effective Go - Testing
- Blog oficial: Table-driven tests
- Tutorial: Add a test
- Wiki Go: Table-driven tests
Exercícios
-
Crie uma função
Multiplicaque receba dois inteiros e retorne o produto. Escreva um testeTestMultiplicaque verifique o resultado para alguns casos simples.✓ Resposta:package main import "testing" func Multiplica(a, b int) int { return a * b } func TestMultiplica(t *testing.T) { casos := []struct{ a, b, esperado int }{ {2, 3, 6}, {0, 5, 0}, {-1, 4, -4}, } for _, c := range casos { got := Multiplica(c.a, c.b) if got != c.esperado { t.Errorf("Multiplica(%d, %d) = %d; esperado %d", c.a, c.b, got, c.esperado) } } } -
Escreva uma função
Divideque retorne o quociente e o resto da divisão de dois inteiros. Crie um teste table-driven para verificar vários casos, incluindo divisão por zero (trate o erro).✓ Resposta:package main import "testing" func Divide(a, b int) (int, int, error) { if b == 0 { return 0, 0, errors.New("divisão por zero") } return a / b, a % b, nil } func TestDivide(t *testing.T) { casos := []struct { nome string a, b int quoc, resto int erro bool }{ {"divisão normal", 10, 3, 3, 1, false}, {"divisão exata", 8, 2, 4, 0, false}, {"por zero", 5, 0, 0, 0, true}, } for _, c := range casos { t.Run(c.nome, func(t *testing.T) { q, r, err := Divide(c.a, c.b) if (err != nil) != c.erro { t.Errorf("erro esperado? %v; obtido %v", c.erro, err) } if q != c.quoc || r != c.resto { t.Errorf("Divide(%d, %d) = (%d, %d); esperado (%d, %d)", c.a, c.b, q, r, c.quoc, c.resto) } }) } } -
Usando
t.Run, escreva um teste para uma funçãoPalindromo(s string) boolque verifica se a string é um palíndromo. Inclua casos com letras maiúsculas e espaços (ignore espaços e case).✓ Resposta:package main import ( "strings" "testing" ) func Palindromo(s string) bool { s = strings.ToLower(strings.ReplaceAll(s, " ", "")) for i := 0; i < len(s)/2; i++ { if s[i] != s[len(s)-1-i] { return false } } return true } func TestPalindromo(t *testing.T) { casos := []struct { nome string s string esperado bool }{ {"palíndromo simples", "arara", true}, {"com espaços", "Socorram me subi no onibus em Marrocos", true}, {"não palíndromo", "golang", false}, } for _, c := range casos { t.Run(c.nome, func(t *testing.T) { got := Palindromo(c.s) if got != c.esperado { t.Errorf("Palindromo(%q) = %v; esperado %v", c.s, got, c.esperado) } }) } } -
Escreva um benchmark para a função
Somaque soma dois inteiros. Use a flag-benchpara executá-lo e observe o resultado.✓ Resposta:package main import "testing" func Soma(a, b int) int { return a + b } func BenchmarkSoma(b *testing.B) { for i := 0; i < b.N; i++ { Soma(1, 2) } }Execute com:
go test -bench=. -
Crie um teste para uma função
Fatorial(n int) (int, error)que retorna erro para n negativo ou maior que 20 (para evitar overflow). Use table-driven tests e verifique os casos de erro.✓ Resposta:package main import ( "errors" "testing" ) func Fatorial(n int) (int, error) { if n < 0 || n > 20 { return 0, errors.New("n fora do intervalo") } resultado := 1 for i := 2; i <= n; i++ { resultado *= i } return resultado, nil } func TestFatorial(t *testing.T) { casos := []struct { nome string n int esperado int erro bool }{ {"fatorial de 0", 0, 1, false}, {"fatorial de 5", 5, 120, false}, {"negativo", -1, 0, true}, {"muito grande", 21, 0, true}, } for _, c := range casos { t.Run(c.nome, func(t *testing.T) { got, err := Fatorial(c.n) if (err != nil) != c.erro { t.Errorf("erro esperado? %v; obtido %v", c.erro, err) } if got != c.esperado { t.Errorf("Fatorial(%d) = %d; esperado %d", c.n, got, c.esperado) } }) } }