<h2>O que é Fuzzing e por que você deveria se importar</h2>
<p>Fuzzing é uma técnica de teste automatizado que alimenta seu programa com dados aleatórios ou semi-aleatórios para descobrir comportamentos inesperados, bugs de segurança e crashes. Diferente dos testes unitários tradicionais, onde você escreve casos de teste específicos, o fuzzing gera centenas de milhares de entradas em busca de falhas que você não previu. Isso é particularmente valioso para funções que processam dados não confiáveis: parsers, validadores, criptografia e qualquer coisa que trabalhe com entrada do usuário.</p>
<p>A grande vantagem do fuzzing em Go é que ele está integrado nativamente ao <code>go test</code> desde a versão 1.18. Você não precisa instalar ferramentas externas complexas ou aprender uma sintaxe estranha. É simples, poderoso e funciona de forma eficiente graças ao mecanismo de cobertura de código que o Go mantém internamente. Quando você escreve um fuzz test e o Go o executa, ele não apenas testa valores aleatórios—aprende quais entradas causam comportamentos interessantes e reutiliza essas pistas para gerar novas entradas mais prováveis de encontrar bugs.</p>
<h2>Conceitos Fundamentais: Testes Baseados em Propriedades</h2>
<h3>O que é um teste baseado em propriedades?</h3>
<p>Um teste baseado em propriedades não verifica um resultado exato. Em vez disso, você define uma <em>propriedade</em> que deve ser verdadeira para toda entrada válida. Por exemplo: "se eu criptografar uma mensagem e depois descriptografá-la, devo obter a mensagem original", ou "a função de ordenação nunca deve retornar um array com menos elementos que o input". O fuzzer então tenta quebrar essas propriedades.</p>
<p>Essa abordagem é mais poderosa que testes convencionais porque você não precisa pensar em todos os casos extremos—o fuzzer faz isso por você. A máquina consegue explorar combinações de dados muito mais rápido do que qualquer humano conseguiria escrever manualmente.</p>
<h3>Como funciona o fuzzing em Go</h3>
<p>Quando você escreve uma função <code>FuzzXxx</code> (um fuzz target), o Go a executa de duas formas distintas. Primeiro, em modo <em>seed</em>, ele testa com dados que você forneceu manualmente no arquivo <code>testdata/fuzz/</code>. Depois, em modo <em>coverage-guided</em>, o fuzzer gera novos dados aleatórios baseado em feedback: se uma entrada atinge código não explorado, o fuzzer a salva e a usa como base para gerar variações.</p>
<p>Isso significa que o fuzzer é inteligente—ele não apenas joga números aleatórios. Ele mapeia o caminho de execução do seu código e tenta encontrar caminhos que ainda não foram tocados. Quando encontra algo que causa pânico ou falha de asserção, salva essa entrada como um "corpus" no seu repositório para garantir que o bug não reaparece depois.</p>
<h2>Estrutura e Sintaxe de um Fuzz Test</h2>
<h3>Escrevendo seu primeiro fuzz test</h3>
<p>Um fuzz test em Go segue um padrão simples. A função recebe <code><em>testing.F</code> como parâmetro, não <code></em>testing.T</code> como nos testes normais. Você usa <code>f.Add()</code> para adicionar seeds (valores iniciais) e <code>f.Fuzz()</code> para definir a lógica que será testada contra dados aleatórios.</p>
<pre><code class="language-go">package strings
import (
"testing"
)
func FuzzReverseString(f *testing.F) {
// Seeds: valores iniciais que o fuzzer deve sempre testar
f.Add("hello")
f.Add("")
f.Add("🎉")
// Fuzz: a função que será chamada com dados aleatórios
f.Fuzz(func(t *testing.T, input string) {
// Teste a propriedade aqui
reversed := ReverseString(input)
doubleReversed := ReverseString(reversed)
// A propriedade: reverter duas vezes volta ao original
if input != doubleReversed {
t.Fatalf("Double reverse failed: %q != %q", input, doubleReversed)
}
})
}
func ReverseString(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)
}</code></pre>
<p>Execute com <code>go test -fuzz=FuzzReverseString -fuzztime=10s</code>. O Go alimentará sua função com strings aleatórias por 10 segundos, procurando qualquer entrada que viole a propriedade. Se encontrar uma, salva a entrada problemática em <code>testdata/fuzz/FuzzReverseString/</code> para reprodução futura.</p>
<h3>Tipos suportados pelo fuzzer</h3>
<p>O Go suporta um conjunto limitado de tipos para gerar automaticamente: <code>string</code>, <code>[]byte</code>, <code>rune</code>, <code>byte</code>, <code>int</code>, <code>int8/16/32/64</code>, <code>uint</code>, <code>uint8/16/32/64</code>, <code>bool</code> e <code>float32/64</code>. Se sua função precisa de tipos personalizados, você encadeia múltiplas seeds ou usa reflection manual. Na prática, a maioria dos casos reais usa apenas <code>string</code> e <code>[]byte</code>.</p>
<pre><code class="language-go">func FuzzParseJSON(f *testing.F) {
f.Add([]byte({"name": "John"}))
f.Add([]byte([]))
f.Add([]byte(``))
f.Fuzz(func(t *testing.T, input []byte) {
// O fuzzer vai gerar []byte aleatórios
var result interface{}
_ = json.Unmarshal(input, &result)
// Se panicar, o fuzzer detecta
})
}</code></pre>
<h2>Casos Reais: Encontrando Bugs com Fuzzing</h2>
<h3>Exemplo 1: Validador de Email</h3>
<p>Vamos considerar uma função que valida emails. Ela é fácil de escrever errado porque email é complexo (RFC 5322 é um pesadelo). Fuzzing pode encontrar casos que quebram sua expressão regular:</p>
<pre><code class="language-go">package email
import (
"regexp"
"testing"
)
var emailRegex = regexp.MustCompile(^[a-zA-Z0-9.+_-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$)
func IsValidEmail(email string) bool {
return emailRegex.MatchString(email)
}
func FuzzEmailValidator(f *testing.F) {
f.Add("user@example.com")
f.Add("invalid.email")
f.Add("")
f.Add("@")
f.Fuzz(func(t *testing.T, input string) {
result := IsValidEmail(input)
// Propriedade: se começa com @, é inválido
if input != "" && input[0] == '@' && result {
t.Fatalf("Invalid email passed: %q", input)
}
// Propriedade: se não tem @, é inválido
if !containsChar(input, '@') && result {
t.Fatalf("Email without @ passed: %q", input)
}
})
}
func containsChar(s string, c byte) bool {
for i := 0; i < len(s); i++ {
if s[i] == c {
return true
}
}
return false
}</code></pre>
<p>Quando você roda <code>go test -fuzz=FuzzEmailValidator</code>, o fuzzer pode encontrar entradas como <code>"..@.."</code> ou <code>"a@b"</code> que passam na regex mas violam propriedades lógicas reais.</p>
<h3>Exemplo 2: Função de Codec (Encode/Decode)</h3>
<p>Qualquer função que codifica e depois decodifica é perfeita para fuzzing. A propriedade é simples: "round-trip" deve restaurar os dados originais.</p>
<pre><code class="language-go">package codec
import (
"encoding/base64"
"testing"
)
func FuzzBase64RoundTrip(f *testing.F) {
f.Add([]byte("hello"))
f.Add([]byte(""))
f.Add([]byte{0, 1, 2, 255})
f.Fuzz(func(t *testing.T, input []byte) {
// Encode
encoded := base64.StdEncoding.EncodeToString(input)
// Decode
decoded, err := base64.StdEncoding.DecodeString(encoded)
if err != nil {
t.Fatalf("Decode failed after encode: %v", err)
}
// Propriedade: deve recuperar exatamente
if len(input) != len(decoded) {
t.Fatalf("Length mismatch: %d != %d", len(input), len(decoded))
}
for i := range input {
if input[i] != decoded[i] {
t.Fatalf("Byte mismatch at index %d", i)
}
}
})
}</code></pre>
<p>Este teste é trivial porque <code>base64</code> é confiável, mas imagine aplicá-lo a um codec customizado—o fuzzer acharia qualquer falha de round-trip rapidamente.</p>
<h3>Exemplo 3: Detecção de Panics</h3>
<p>Fuzzing também é excelente para encontrar panics inesperados. Se sua função pânica com entrada válida, o fuzzer captura:</p>
<pre><code class="language-go">package parser
import (
"strconv"
"testing"
)
func ParseAndDouble(input string) (int, error) {
num, err := strconv.Atoi(input)
if err != nil {
return 0, err
}
return num * 2, nil
}
func FuzzParseAndDouble(f *testing.F) {
f.Add("42")
f.Add("0")
f.Add("-1")
f.Fuzz(func(t *testing.T, input string) {
// Se isso panicar com qualquer string, o fuzzer detecta
result, err := ParseAndDouble(input)
// Propriedade: se não houve erro, resultado deve ser par
if err == nil && result%2 != 0 {
t.Fatalf("Result not even: %d", result)
}
})
}</code></pre>
<p>Se houvesse um bug que causasse panic (como divisão por zero escondida), o fuzzer o encontraria e salvaria a entrada problemática.</p>
<h2>Boas Práticas e Otimizações</h2>
<h3>Mantendo fuzz tests rápidos</h3>
<p>Seu fuzz test será executado bilhões de vezes em ambientes CI/CD, então eficiência é crítica. Evite operações caras dentro de <code>f.Fuzz()</code>: não faça chamadas HTTP, não acesse banco de dados, não escreva em disco. Se precisar, use <code>testdata/</code> para fixtures, não para I/O dinâmico.</p>
<pre><code class="language-go"></code></pre>
<h3>Organizando seeds com testdata</h3>
<p>Crie uma estrutura de diretórios <code>testdata/fuzz/FuzzName/</code> para seeds:</p>
<pre><code>project/
├── main.go
├── main_test.go
└── testdata/
└── fuzz/
└── FuzzParseConfig/
├── seed1
├── seed2
└── seed3</code></pre>
<p>Cada arquivo contém um seed binário. Você pode criar manualmente ou deixar o fuzzer gerar e depois comitar interessantes:</p>
<pre><code class="language-bash">go test -fuzz=FuzzParseConfig -fuzztime=1m
Go vai salvar entradas interessantes em testdata automaticamente
git add testdata/
git commit -m "Add fuzz seeds"</code></pre>
<h3>Interpretando resultados</h3>
<p>Quando o fuzzer encontra um crash:</p>
<pre><code>--- FAIL: FuzzReverseString (0.01s)
--- FAIL: FuzzReverseString (0.01s)
fuzz.go:23: Double reverse failed: "hello" != "olleh"
Failing input written to testdata/fuzz/FuzzReverseString/12e4a8c</code></pre>
<p>Aquele arquivo <code>12e4a8c</code> é binário. O Go pode reproduced o erro:</p>
<pre><code class="language-bash">go test -run FuzzReverseString/12e4a8c</code></pre>
<p>Ele testa especificamente aquela entrada. Muito útil para debugging.</p>
<h2>Conclusão</h2>
<p>Fuzzing em Go não é um recurso avançado ou opcional—é uma ferramenta fundamental que você deveria incorporar imediatamente. Primeiro, aprenda que um teste fuzzy define uma propriedade que deve ser sempre verdadeira, não um resultado específico. Segundo, entenda que o Go faz o trabalho pesado: você escreve a lógica, ele gera os dados inteligentemente. Terceiro, use-o em funções que processam dados não confiáveis: parsers, validadores, codecs—qualquer coisa onde um input inesperado pode quebrar seu programa. Comece simples com round-trip tests e propriedades óbvias, depois evolua para lógica mais complexa conforme ganhar experiência.</p>
<h2>Referências</h2>
<ul>
<li><a href="https://go.dev/doc/fuzz/" target="_blank" rel="noopener noreferrer">Go Fuzzing Official Documentation</a></li>
<li><a href="https://dave.cheney.net/2020/07/02/fuzz-testing-in-go" target="_blank" rel="noopener noreferrer">Dave Cheney - Fuzz Testing in Go</a></li>
<li><a href="https://go.dev/blog/fuzz-landing" target="_blank" rel="noopener noreferrer">Go Release Notes 1.18 - Native Fuzzing Support</a></li>
<li><a href="https://www.fuzzingbook.org/" target="_blank" rel="noopener noreferrer">Fuzzing Book - Introduction to Fuzzing</a></li>
<li><a href="https://pkg.go.dev/testing#hdr-Fuzzing" target="_blank" rel="noopener noreferrer">Go Standard Library Testing Package</a></li>
</ul>
<p><!-- FIM --></p>