Gafes em Go: categorias de testes
Tirei 11 no D12, então vamos à primeira dica do capítulo 11, Testing, do livro 100 Go Mistakes and How to Avoid Them, de Teiva Harsanyi.
O título original da gafe #86 é Not categorizing tests
(não categorizar testes).
A dica é:
saiba categorizar testes!
Harsanyi começa citando as categorias da pirâmide de testes:
- base da pirâmide: testes unitários, mais numerosos, simples, rápidos;
- meio da pirâmide: teste de integração, menor quantidade, complicados, mais lentos;
- topo da pirâmide: testes E2E (End-to-End, literalmente “ponta-a-ponta”), poucos, bem complicados, bem lentos.
A ideia da dica #86 é mostrar como controlar a execução dos testes conforme sua categoria.
Por exemplo, num ambiente configurado para rodar testes toda vez que você salva o código, é melhor rodar só os testes unitários, que são mais rápidos. Mas antes de subir uma nova versão para a produção, é bom rodar todos os testes até E2E.
Daí o autor apresenta três formas de categorizar os testes para escolher quais rodar.
Vamos começar pela opção -short que já vem pronta para usar.
Opção -short
Um forma simples e prática de categorizar testes: “curtos” ou “longos”.
O pacote testing tem suporte para estas categorias.
Quando você roda go test -short, a função testing.Short()
devolve true e então você
pode pular os testes longos com T.Skip.
Exemplo
Mandaram você implementar uma função que devolve o N-ésimo item da série de Fibonacci usando o algoritmo duplamente recursivo, que é exponencialmente ineficiente: O(φn).
Implementação: fiboshort/main.go
package fibo
// Fibonacci computes the Nth number in the Fibonacci series
// using utterly slow double recursion to show how to manage
// slow tests.
func Fibonacci(n int) int {
if n <= 1 {
return n
}
return Fibonacci(n-1) + Fibonacci(n-2)
}
Testes: fiboshort/main_test.go
package fibo
import (
"testing"
)
func TestFibonacci(t *testing.T) {
tests := []struct {
n int
expected int
}{
{0, 0},
{1, 1},
{2, 1},
{3, 2},
{10, 55},
{22, 17711},
}
for _, tt := range tests {
result := Fibonacci(tt.n)
if result != tt.expected {
t.Errorf("fibonacci(%d) = %d; want %d", tt.n, result, tt.expected)
}
}
}
func TestFibonacciSlow(t *testing.T) {
if testing.Short() {
t.Skip("skipped long test")
}
var given = 44
result := Fibonacci(given)
expected := 701408733
if result != expected {
t.Errorf("Fibonacci(%d) = %d; want %d", given, result, expected)
}
}
Testes com n até 40 são OK, mas logo começa a levar segundos para cada teste.
Por isso o teste TestFibonacciSlow é pulado quanto testing.Short é verdadeiro.
Configurado assim, TestFibonacciSlow roda por padrão:
% go test -v
=== RUN TestFibonacci
--- PASS: TestFibonacci (0.00s)
=== RUN TestFibonacciSlow
--- PASS: TestFibonacciSlow (2.15s)
PASS
ok fibo 2.514s
Mas passando a opção -short você evita o teste lento:
% go test -v -short
=== RUN TestFibonacci
--- PASS: TestFibonacci (0.00s)
=== RUN TestFibonacciSlow
main_test.go:30: skipped long test
--- SKIP: TestFibonacciSlow (0.00s)
PASS
ok fibo 0.184s
Este esquema é bom, mas só permite duas categorias de testes: curtos e longos.
O próximo esquema é mais flexível.
Skip com variável de ambiente
Harsanyi cita
um post
de Peter Bourgon, que também usa o método T.Skip
após testar se uma variável de ambiente está configurada.
O código novo está em fiboenv/main_test.go:
package fibo
import (
"os"
"testing"
)
func TestFibonacci(t *testing.T) {
tests := []struct {
n int
expected int
}{
{0, 0},
{1, 1},
{2, 1},
{3, 2},
{10, 55},
{22, 17711},
}
for _, tt := range tests {
result := Fibonacci(tt.n)
if result != tt.expected {
t.Errorf("fibonacci(%d) = %d; want %d", tt.n, result, tt.expected)
}
}
}
func TestFibonacciSlow(t *testing.T) {
if os.Getenv("ALL") != "true" {
t.Skip("skipped; to run, set ALL=true")
}
var given = 44
result := Fibonacci(given)
expected := 701408733
if result != expected {
t.Errorf("Fibonacci(%d) = %d; want %d", given, result, expected)
}
}
Agora por padrão o teste lento não roda, mas há um aviso:
% go test -v
=== RUN TestFibonacci
--- PASS: TestFibonacci (0.00s)
=== RUN TestFibonacciSlow
main_test.go:31: skipped; to run, set ALL=true
--- SKIP: TestFibonacciSlow (0.00s)
PASS
ok fibo 0.186s
Para incluir o teste lento, defina ALL=true antes de chamar go test:
% ALL=true go test -v
=== RUN TestFibonacci
--- PASS: TestFibonacci (0.00s)
=== RUN TestFibonacciSlow
--- PASS: TestFibonacciSlow (2.14s)
PASS
ok fibo 2.324s
Com este esquema você pode ter várias categorias de testes
associadas a variávels de ambiente como INTEGRATION, E2E,
FUZZ etc.
Achei excelente esta sugestão. Vou adotar.
Build tags
O terceiro jeito de categorizar testes é usando build tags (etiquetas de compilação?).
Uma build tag é uma marcação assim no topo de um arquivo .go:
//go:build slow
Com este tag, o arquivo só será compilado se a opção --tags=slow for passada na linha de comando do compilador.
O tag vale para o arquivo todo, então neste caso precisamos separar os testes em dois ou mais arquivos.
Por exemplo, colocando o teste lento em fibotag/hard_test.go:
//go:build slow
package fibo
import "testing"
func TestFibonacciSlow(t *testing.T) {
var given = 44
result := Fibonacci(given)
expected := 701408733
if result != expected {
t.Errorf("Fibonacci(%d) = %d; want %d", given, result, expected)
}
}
Repare na build tag. Com ela, este arquivo fibotag/hard_test.go não será compilado por padrão.
Este comando roda só os testes de main_test.go:
% go test -v
=== RUN TestFibonacci
--- PASS: TestFibonacci (0.00s)
PASS
ok fibo 0.186s
Para rodar os testes lentos, inclua a opção -tags=slow na linha de comando:
% go test -v -tags=slow
=== RUN TestFibonacciSlow
--- PASS: TestFibonacciSlow (2.13s)
=== RUN TestFibonacci
--- PASS: TestFibonacci (0.00s)
PASS
ok fibo 2.311s
Se você quiser que -tags=slow execute só o teste em hard_test_go, você pode colocar
a tag com sinal ! (negação lógica) no main_test.go:
//go:build !slow
Este método funciona, mas os testes são silenciosamente ignorados, o que pode causar problemas.
Por isso preferi a opção Skip com variável de ambiente.
Opinião do Ramalho
No livro 100 Go Mistakes o autor apresente estas três técnicas na ordem inversa, começando por build tags.
Decidi mostrar a opção -short primeiro porque é a mais simples, já vem pronta.
Skip com variável de ambiente vem a seguir,
pois também usa uma lógica para chamar T.Skip
sob determinada condição.
Finalmente, vimos as build tags, que Peter Bourgon considera “esotéricas”, mas Harsanyi apresenta primeiro. O título do post de Bourgon já diz tudo: Don’t use build tags for integration tests (Não use build tags para testes de integração). Concordo.
Referências
Customizing Go Binaries with Build Tags: boa explicação sobre build tags, porém usando a sintaxe
//+build barrilem vez de//go:build barrilque virou padrão a partir do Go 1.17, segundo Harsanyi na página 263 do seu livro.How to properly use build tags?: respostas no Stack Overflow.