Blog do Ramalho.org

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:

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

Tags: