Go

Boas Práticas de Fuzzing em Go: Testes Baseados em Propriedades com go test -fuzz para Times Ágeis

12 min de leitura

Boas Práticas de Fuzzing em Go: Testes Baseados em Propriedades com go test -fuzz para Times Ágeis

O que é Fuzzing e por que você deveria se importar 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. A grande vantagem do fuzzing em Go é que ele está integrado nativamente ao 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

<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: &quot;se eu criptografar uma mensagem e depois descriptografá-la, devo obter a mensagem original&quot;, ou &quot;a função de ordenação nunca deve retornar um array com menos elementos que o input&quot;. 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 &quot;corpus&quot; 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 (

&quot;testing&quot;

)

func FuzzReverseString(f *testing.F) {

// Seeds: valores iniciais que o fuzzer deve sempre testar

f.Add(&quot;hello&quot;)

f.Add(&quot;&quot;)

f.Add(&quot;🎉&quot;)

// 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(&quot;Double reverse failed: %q != %q&quot;, input, doubleReversed)

}

})

}

func ReverseString(s string) string {

runes := []rune(s)

for i, j := 0, len(runes)-1; i &lt; 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({&quot;name&quot;: &quot;John&quot;}))

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, &amp;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 (

&quot;regexp&quot;

&quot;testing&quot;

)

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(&quot;user@example.com&quot;)

f.Add(&quot;invalid.email&quot;)

f.Add(&quot;&quot;)

f.Add(&quot;@&quot;)

f.Fuzz(func(t *testing.T, input string) {

result := IsValidEmail(input)

// Propriedade: se começa com @, é inválido

if input != &quot;&quot; &amp;&amp; input[0] == &#039;@&#039; &amp;&amp; result {

t.Fatalf(&quot;Invalid email passed: %q&quot;, input)

}

// Propriedade: se não tem @, é inválido

if !containsChar(input, &#039;@&#039;) &amp;&amp; result {

t.Fatalf(&quot;Email without @ passed: %q&quot;, input)

}

})

}

func containsChar(s string, c byte) bool {

for i := 0; i &lt; 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>&quot;..@..&quot;</code> ou <code>&quot;a@b&quot;</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: &quot;round-trip&quot; deve restaurar os dados originais.</p>

<pre><code class="language-go">package codec

import (

&quot;encoding/base64&quot;

&quot;testing&quot;

)

func FuzzBase64RoundTrip(f *testing.F) {

f.Add([]byte(&quot;hello&quot;))

f.Add([]byte(&quot;&quot;))

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(&quot;Decode failed after encode: %v&quot;, err)

}

// Propriedade: deve recuperar exatamente

if len(input) != len(decoded) {

t.Fatalf(&quot;Length mismatch: %d != %d&quot;, len(input), len(decoded))

}

for i := range input {

if input[i] != decoded[i] {

t.Fatalf(&quot;Byte mismatch at index %d&quot;, 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 (

&quot;strconv&quot;

&quot;testing&quot;

)

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(&quot;42&quot;)

f.Add(&quot;0&quot;)

f.Add(&quot;-1&quot;)

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 &amp;&amp; result%2 != 0 {

t.Fatalf(&quot;Result not even: %d&quot;, 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 &quot;Add fuzz seeds&quot;</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: &quot;hello&quot; != &quot;olleh&quot;

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>&lt;!-- FIM --&gt;</p>

Comentários

Mais em Go

Guia Completo de Chi Router em Go: Roteamento Idiomático e Middlewares Componíveis
Guia Completo de Chi Router em Go: Roteamento Idiomático e Middlewares Componíveis

O que é Chi e Por que Ele Importa Chi é um roteador HTTP leve e expressivo pa...

Como Usar Structs em Go: Definição, Embedding e Métodos em Produção
Como Usar Structs em Go: Definição, Embedding e Métodos em Produção

Structs em Go: Definição, Embedding e Métodos O que é uma Struct e Por Que Us...

Guia Completo de Gin em Go: Framework Web de Alta Performance na Prática
Guia Completo de Gin em Go: Framework Web de Alta Performance na Prática

O que é Gin e Por Que Escolher Este Framework Gin é um framework web escrito...