Testes em Go
Nesta aula, você aprenderá a escrever testes em Go usando o pacote testing, a convenção func TestXxx, o comando go test e a técnica de table-driven tests, com exemplos práticos e boas práticas para criar testes eficientes e legíveis.
Testes são uma parte essencial do desenvolvimento de software em Go. A linguagem foi projetada com testes em mente, oferecendo um pacote padrão chamado testing que fornece todas as ferramentas necessárias para escrever testes unitários, benchmarks e exemplos. Nesta aula, você aprenderá os fundamentos de testes em Go, desde a criação de funções de teste até técnicas avançadas como testes orientados por tabela.
Escrever testes em Go é simples e direto. O compilador e as ferramentas da linguagem integram-se perfeitamente ao fluxo de trabalho, permitindo que você rode testes com um único comando: go test. Vamos explorar cada aspecto em detalhes, com exemplos práticos que você pode adaptar aos seus próprios projetos.
Pacote testing
O pacote testing é a base para a escrita de testes em Go. Ele fornece o tipo T, que é usado em funções de teste para relatar falhas e controlar o fluxo do teste. Além disso, o pacote oferece suporte a benchmarks (B) e exemplos (Example), mas o foco principal aqui são os testes unitários.
Para usar o pacote, você precisa importá-lo no seu arquivo de teste. Arquivos de teste devem ter o sufixo _test.go e geralmente ficam no mesmo pacote do código que estão testando. Por exemplo, se você tem um arquivo math.go com funções matemáticas, o teste correspondente estará em math_test.go.
O tipo T possui métodos importantes como Error, Fatal, Log e Fail. O método Error registra uma falha e continua a execução do teste, enquanto Fatal registra uma falha e interrompe imediatamente o teste. Log registra uma mensagem de log, útil para depuração.
package math
import "testing"
func TestAdd(t *testing.T) {
result := Add(2, 3)
if result != 5 {
t.Errorf("Add(2, 3) = %d; want 5", result)
}
}Neste exemplo, a função TestAdd testa uma função Add. Se o resultado não for o esperado, o teste falha com uma mensagem clara.
func TestXxx
A convenção para funções de teste em Go é nomeá-las começando com Test, seguido por uma descrição do que está sendo testado (geralmente o nome da função). A assinatura deve ser func TestXxx(t *testing.T), onde Xxx pode ser qualquer nome que comece com letra maiúscula (para ser exportado) ou minúscula. O parâmetro t *testing.T é obrigatório.
Essas funções são reconhecidas automaticamente pelo comando go test. Não é necessário registrá-las explicitamente; basta definir a função no arquivo _test.go. O go test varre o pacote e executa todas as funções que seguem essa convenção.
Um ponto importante: o nome da função deve ser único dentro do pacote de teste. Se você tiver dois testes com o mesmo nome, o compilador reclamará. Além disso, é comum usar nomes descritivos que indiquem o comportamento esperado, como TestDivisionByZero ou TestParseValidInput.
package main
import "testing"
func TestIsEven(t *testing.T) {
if !IsEven(4) {
t.Error("expected 4 to be even")
}
}
func TestIsOdd(t *testing.T) {
if !IsOdd(3) {
t.Error("expected 3 to be odd")
}
}No exemplo acima, temos duas funções de teste, cada uma verificando um aspecto diferente da lógica. Isso mantém os testes organizados e facilita a localização de falhas.
go test
O comando go test é a ferramenta central para executar testes em Go. Ele compila o pacote atual (ou o pacote especificado) e executa todas as funções de teste encontradas. O comando oferece várias flags para controlar o comportamento, como -v (verbose) para exibir detalhes de cada teste, -run para filtrar testes por nome, e -cover para medir a cobertura de código.
Para rodar testes em um pacote específico, você pode usar go test ./pkg/... ou go test ./... para todos os pacotes. A saída padrão mostra um resumo: se todos os testes passarem, exibe ok; caso contrário, mostra falhas detalhadas.
Exemplo de execução com verbosidade:
$ go test -v
=== RUN TestAdd
--- PASS: TestAdd (0.00s)
=== RUN TestSubtract
--- PASS: TestSubtract (0.00s)
PASS
ok exemplo/matematica 0.002sO comando go test também suporta a execução de benchmarks e exemplos, mas esses são tópicos avançados. Para a maioria dos casos, você usará apenas as funções de teste.
Uma dica: use go test -run TestNome para executar apenas um teste específico, útil durante o desenvolvimento.
Table-driven tests
Table-driven tests (testes orientados por tabela) são uma técnica poderosa em Go para testar múltiplos casos de entrada de forma concisa e legível. Em vez de escrever um bloco if para cada caso, você define uma tabela com entradas e saídas esperadas, e itera sobre ela usando um loop.
Essa abordagem reduz a duplicação de código, facilita a adição de novos casos e torna os testes mais legíveis. É uma prática recomendada pela comunidade Go e amplamente utilizada em projetos reais.
A estrutura básica é uma slice de structs, onde cada struct representa um caso de teste com campos como name, input e expected. Dentro do loop, você executa a função sendo testada e compara o resultado com o esperado, usando t.Run para executar subtestes com nomes descritivos.
package math
import "testing"
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
}{
{"positive numbers", 2, 3, 5},
{"negative numbers", -2, -3, -5},
{"zero values", 0, 0, 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := Add(tt.a, tt.b)
if result != tt.expected {
t.Errorf("Add(%d, %d) = %d; want %d", tt.a, tt.b, result, tt.expected)
}
})
}
}Neste exemplo, a tabela tem três casos. O loop itera sobre eles e executa um subteste para cada um. Se um caso falhar, a mensagem incluirá o nome do subteste, facilitando a identificação do problema.
Table-driven tests são especialmente úteis quando você tem muitas variações de entrada e saída. Eles também incentivam a pensar em casos de borda, como valores negativos, zero, valores extremos, etc.
Boas práticas e observações finais
Ao escrever testes em Go, lembre-se de:
- Testar casos de borda e entradas inválidas, além dos casos felizes.
- Usar nomes descritivos para testes e subtestes.
- Manter os testes independentes e isolados; cada teste deve ser executável sozinho.
- Evitar depender de ordem de execução ou de estado global.
- Usar
t.Helper()em funções auxiliares para melhorar mensagens de erro.
Testes são parte integrante do desenvolvimento em Go. Com o pacote testing, você pode escrever testes robustos e manter a qualidade do seu código. Pratique os conceitos desta aula e explore mais recursos, como benchmarks e exemplos.
Referências
- Documentação oficial do pacote testing
- Como escrever código Go
- Artigo sobre table-driven tests do Go Blog
- Effective Go
- Go Playground
Exercícios
- Escreva uma função
Divideque receba dois inteiros e retorne o quociente e um erro se o divisor for zero. Crie um testeTestDivideque verifique o resultado para divisões normais e o erro para divisão por zero.
package main
import "errors"
func Divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}package main
import "testing"
func TestDivide(t *testing.T) {
// Caso normal
result, err := Divide(10, 2)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result != 5 {
t.Errorf("Divide(10, 2) = %d; want 5", result)
}
// Divisão por zero
_, err = Divide(10, 0)
if err == nil {
t.Error("expected error for division by zero")
}
}IsPalindrome que verifica se uma string é um palíndromo (ignorando maiúsculas/minúsculas e espaços). Inclua casos como "ana", "arara", "hello", "A man a plan a canal Panama".package main
import "testing"
func TestIsPalindrome(t *testing.T) {
tests := []struct {
name string
input string
want bool
}{
{"simple palindrome", "ana", true},
{"another palindrome", "arara", true},
{"not palindrome", "hello", false},
{"palindrome with spaces", "A man a plan a canal Panama", 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; want %v", tt.input, got, tt.want)
}
})
}
}go test -v e explique a saída. O que acontece se um teste falhar?Ao executar go test -v, cada teste é listado com seu nome e status (PASS ou FAIL). Se um teste falhar, o Go imprime a mensagem de erro e o teste é marcado como FAIL. No final, o comando retorna um código de saída diferente de zero, indicando falha. Por exemplo, se a função IsPalindrome não estiver implementada corretamente, você verá algo como:
=== RUN TestIsPalindrome
=== RUN TestIsPalindrome/simple_palindrome
main_test.go:20: IsPalindrome("ana") = false; want true
--- FAIL: TestIsPalindrome (0.00s)
--- FAIL: TestIsPalindrome/simple_palindrome (0.00s)
FAILIsso ajuda a identificar rapidamente qual caso falhou.
t.Run com subtestes e inclua um caso de teste que verifique a função com uma string vazia.package main
import "testing"
func TestIsPalindrome(t *testing.T) {
tests := []struct {
name string
input string
want bool
}{
{"simple palindrome", "ana", true},
{"another palindrome", "arara", true},
{"not palindrome", "hello", false},
{"palindrome with spaces", "A man a plan a canal Panama", true},
{"empty string", "", true}, // string vazia é considerada palíndromo?
}
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; want %v", tt.input, got, tt.want)
}
})
}
}Fatorial que calcula o fatorial de um número inteiro não negativo. Use table-driven tests para cobrir os casos 0, 1, 5 e um caso que espera erro para números negativos.package main
import "errors"
func Fatorial(n int) (int, error) {
if n < 0 {
return 0, errors.New("negative number")
}
result := 1
for i := 2; i <= n; i++ {
result *= i
}
return result, nil
}package main
import "testing"
func TestFatorial(t *testing.T) {
tests := []struct {
name string
input int
want int
wantErr bool
}{
{"zero", 0, 1, false},
{"one", 1, 1, false},
{"five", 5, 120, false},
{"negative", -1, 0, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Fatorial(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("Fatorial(%d) error = %v, wantErr %v", tt.input, err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("Fatorial(%d) = %d; want %d", tt.input, got, tt.want)
}
})
}
}