Quando os testes unitários mentem e os bugs permanecem
Você escreveu 20 testes unitários. Todos verdes. O CI passa. Você está satisfeito. E então — um bug em produção. Familiar? Cenário clássico: você testou apenas os casos que você mesmo imaginou. E a realidade, como de propósito, apresenta combinações que você nem sequer considerou.
Em Go existe uma abordagem poderosa, mas subestimada — testes baseados em propriedades (ou teste de propriedades). Em vez de verificar entradas e saídas específicas, você descreve as propriedades que a função deve satisfazer, e o framework gera centenas de dados de teste aleatórios. Se a propriedade for violada — você encontrou um bug.
Neste artigo, vamos explorar como implementar testes baseados em propriedades em Go usando a biblioteca gopter, escrever exemplos reais e ver quais bugs podemos capturar. Vamos nessa!
O que são testes baseados em propriedades e por que você precisa deles?
Imagine que você está testando uma função de ordenação. Um teste unitário comum:
func TestSort(t *testing.T) { input := []int{3, 1, 2} result := sort(input) expected := []int{1, 2, 3} if !reflect.DeepEqual(result, expected) { t.Errorf("got %v, want %v", result, expected) }}Ok, funciona. Mas e se o array estiver vazio? E se todos os elementos forem iguais? E se houver números negativos? Você pode escrever mais 10 testes, mas ainda assim vai deixar algo passar.
Testes baseados em propriedades resolvem esse problema radicalmente: você formula invariantes — propriedades que devem ser verdadeiras para qualquer entrada válida. Por exemplo, para ordenação:
- O comprimento do array ordenado é igual ao comprimento do array original.
- Cada elemento do array original está presente no array ordenado (e vice-versa).
- O array está ordenado em ordem crescente.
O framework (por exemplo, gopter ou o nativo testing/quick) gera dados de entrada aleatórios e verifica se todas as propriedades são atendidas. Se houver um bug — você ficará sabendo, mesmo sem saber onde procurar.
Bibliotecas Go para testes baseados em propriedades
No ecossistema Go existem várias ferramentas:
testing/quick— pacote nativo, minimalista, mas com capacidades limitadas.gopter— biblioteca poderosa com geradores, redução (minimização de dados de entrada) e configurações flexíveis. Recomendamos esta.rapid— alternativa moderna, mais performática.
Usaremos gopter, pois possui melhor documentação e comunidade. Instalação:
go get github.com/leanovate/gopterEscrevendo o primeiro teste baseado em propriedades em Go
Suponha que temos uma função Reverse que inverte uma string:
func Reverse(s string) string { runes := []rune(s) for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 { runes[i], runes[j] = runes[j], runes[i] } return string(runes)}Qual propriedade podemos verificar? Se invertermos a string duas vezes, devemos obter a original:
import ( "testing" "github.com/leanovate/gopter" "github.com/leanovate/gopter/gen")
func TestReverseTwice(t *testing.T) { properties := gopter.NewProperties(nil)
properties.Property("reverse(reverse(s)) == s", prop.ForAll( func(s string) bool { return Reverse(Reverse(s)) == s }, gen.AnyString(), ))
properties.TestingRun(t)}Execute o teste — e o gopter gerará 100 strings aleatórias, verificando a propriedade. Se tudo ok — o teste fica verde. Se não — você receberá um contraexemplo (a menor string na qual o teste falhou).