Fuzz testing (ou fuzzing) é uma técnica que alimenta sua função com entradas aleatórias e mutadas para encontrar bugs que testes manuais jamais encontrariam. Desde o Go 1.18, a linguagem possui suporte nativo a fuzzing integrado ao go test — sem precisar de ferramentas externas.
Neste guia, você vai aprender a escrever fuzz tests, entender como o motor de fuzzing funciona, encontrar bugs reais e integrar fuzzing no seu workflow de desenvolvimento.
O que É Fuzzing e Por que Importa
Testes tradicionais (como os que cobrimos no guia de testes em Go) dependem de casos escritos manualmente. Você testa o cenário feliz, alguns edge cases e pronto. O problema? Você só testa o que imagina.
Fuzzing inverte essa lógica: o motor gera milhares de inputs automaticamente, buscando combinações que causem panic, retornos incorretos ou comportamento inesperado. É particularmente eficaz para:
- Parsers (URLs, JSON, CSV, protocolos customizados)
- Funções de validação (emails, CPFs, inputs de formulário)
- Serialização/deserialização (encoding, marshaling)
- Operações matemáticas com edge cases numéricos
Seu Primeiro Fuzz Test
Um fuzz test em Go segue uma convenção simples: a função começa com Fuzz, recebe *testing.F e usa f.Fuzz() para definir o target:
// reverse.go
package stringutil
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)
}
// reverse_test.go
package stringutil
import (
"testing"
"unicode/utf8"
)
func FuzzReverse(f *testing.F) {
// Seed corpus: valores iniciais para o motor de fuzzing
f.Add("hello")
f.Add("mundo")
f.Add("")
f.Add("日本語")
f.Fuzz(func(t *testing.T, s string) {
reversed := Reverse(s)
doubleReversed := Reverse(reversed)
// Propriedade 1: reverter duas vezes volta ao original
if s != doubleReversed {
t.Errorf("duplo reverse falhou: %q -> %q -> %q", s, reversed, doubleReversed)
}
// Propriedade 2: o tamanho em bytes deve ser mantido
if len(reversed) != len(s) {
t.Errorf("tamanho mudou: %d -> %d", len(s), len(reversed))
}
// Propriedade 3: string válida UTF-8 deve continuar válida
if utf8.ValidString(s) && !utf8.ValidString(reversed) {
t.Errorf("reverse produziu UTF-8 inválido: %q", reversed)
}
})
}
Execute com a flag -fuzz:
# Roda o fuzz test por 30 segundos
go test -fuzz=FuzzReverse -fuzztime=30s
# Roda indefinidamente até encontrar uma falha
go test -fuzz=FuzzReverse
Seed Corpus vs Inputs Gerados
O seed corpus são os valores iniciais que você fornece via f.Add(). O motor de fuzzing usa esses valores como ponto de partida e aplica mutações — invertendo bits, adicionando caracteres, truncando strings, inserindo bytes especiais.
O Go suporta os seguintes tipos como parâmetros de fuzzing:
string,[]byteint,int8,int16,int32,int64uint,uint8,uint16,uint32,uint64float32,float64bool
Cada f.Add() deve ter argumentos que correspondam exatamente aos parâmetros do f.Fuzz():
func FuzzParsePort(f *testing.F) {
// Seed com string e int — mesma ordem do target
f.Add("8080", true)
f.Add("abc", false)
f.Add("99999", false)
f.Add("", false)
f.Fuzz(func(t *testing.T, input string, shouldBeValid bool) {
// ... test logic
})
}
Fuzzing Guiado por Cobertura
O motor de fuzzing do Go usa coverage-guided fuzzing: ele instrumenta o código e prioriza inputs que exploram novos caminhos de execução. Isso significa que o fuzzer não é puramente aleatório — ele evolui seus inputs para maximizar a cobertura de código, encontrando bugs em branches raramente testadas.
Exemplos Práticos: Encontrando Bugs Reais
Fuzzing um Parser de URL
func FuzzParseURL(f *testing.F) {
f.Add("https://golang.com.br/blog")
f.Add("http://localhost:8080/api/v1")
f.Add("ftp://user:pass@host.com")
f.Add("")
f.Fuzz(func(t *testing.T, rawURL string) {
parsed, err := ParseURL(rawURL)
if err != nil {
return // erros são esperados para inputs inválidos
}
// Se parseou com sucesso, reconstruir deve funcionar
reconstructed := parsed.String()
reparsed, err := ParseURL(reconstructed)
if err != nil {
t.Errorf("não conseguiu reparar URL reconstruída: %q -> %q: %v",
rawURL, reconstructed, err)
}
// Propriedade: scheme deve ser preservado
if parsed.Scheme != reparsed.Scheme {
t.Errorf("scheme mudou: %q vs %q", parsed.Scheme, reparsed.Scheme)
}
})
}
Fuzzing Serialização JSON
type Config struct {
Name string `json:"name"`
Port int `json:"port"`
Debug bool `json:"debug"`
}
func FuzzJSONRoundTrip(f *testing.F) {
f.Add("server", 8080, true)
f.Add("", 0, false)
f.Add("名前", -1, true)
f.Fuzz(func(t *testing.T, name string, port int, debug bool) {
original := Config{Name: name, Port: port, Debug: debug}
data, err := json.Marshal(original)
if err != nil {
t.Fatalf("marshal falhou: %v", err)
}
var decoded Config
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("unmarshal falhou: %v", err)
}
if original != decoded {
t.Errorf("roundtrip falhou:\noriginal: %+v\ndecoded: %+v", original, decoded)
}
})
}
Fuzzing uma Calculadora
func FuzzCalculator(f *testing.F) {
f.Add(10.0, 3.0, "+")
f.Add(0.0, 0.0, "/")
f.Add(-1.0, 2.0, "*")
f.Add(math.MaxFloat64, 2.0, "+")
f.Fuzz(func(t *testing.T, a, b float64, op string) {
result, err := Calculate(a, b, op)
if err != nil {
return // operação inválida
}
// Resultado não pode ser NaN para operações válidas
if math.IsNaN(result) {
t.Errorf("NaN inesperado: %f %s %f", a, op, b)
}
})
}
Corrigindo Bugs e o Diretório testdata/corpus
Quando o fuzzer encontra um input que causa falha, ele salva automaticamente em testdata/fuzz/<NomeDaFunção>/. Esse arquivo é adicionado permanentemente ao corpus:
$ go test -fuzz=FuzzReverse
--- FAIL: FuzzReverse (0.50s)
--- FAIL: FuzzReverse/abc123 (0.00s)
reverse_test.go:20: duplo reverse falhou
Failing input written to testdata/fuzz/FuzzReverse/abc123
Importante: sempre faça commit dos arquivos em testdata/fuzz/ no seu repositório. Eles servirão como regressão — o go test normal (sem -fuzz) executa todos os corpus entries como testes regulares.
Fuzzing vs Table-Driven Tests vs Property-Based Tests
| Abordagem | Quando Usar |
|---|---|
| Table-driven tests | Cenários conhecidos, edge cases documentados |
| Fuzzing | Encontrar bugs desconhecidos, testar robustez |
| Property-based tests | Verificar invariantes com inputs aleatórios controlados |
Fuzzing complementa seus testes tradicionais — não os substitui. Use table-driven tests para comportamento esperado e fuzzing para descobrir o inesperado.
Integrando Fuzzing no CI/CD
Fuzzing contínuo é mais eficaz que execuções pontuais. Configure no CI com tempo limitado:
# No seu pipeline de CI
- name: Fuzz Tests
run: go test ./... -fuzz=. -fuzztime=60s
Para projetos que usam Docker, inclua o fuzzing como um estágio separado no build para não bloquear o deploy.
Boas Práticas
- Mantenha fuzz targets rápidos — evite I/O, rede e operações caras dentro do
f.Fuzz() - Minimize alocações — o fuzzer executa milhões de iterações; garbage collection excessiva prejudica a performance (entenda mais sobre GC no artigo sobre o Green Tea GC)
- Teste propriedades, não valores específicos — fuzzing é sobre invariantes (idempotência, roundtrip, ausência de panic)
- Use
t.Skip()para inputs inválidos conhecidos — quando certos inputs não fazem sentido para o domínio - Combine com tratamento de erros robusto — funções que retornam
erroradequadamente são mais fáceis de fuzzar
Limitações do Fuzzing em Go
- Suporta apenas tipos primitivos como parâmetros (sem structs diretamente)
- Um único fuzz target por test function
- Fuzzing de rede/banco de dados requer mocking
- Não substitui testes de integração
Próximos Passos
Fuzzing é uma ferramenta poderosa no arsenal de qualidade do seu código Go. Combine com testes unitários, tratamento de erros robusto e padrões de concorrência para construir software resiliente.
Se você trabalha com APIs, considere fuzzar seus handlers HTTP — veja como estruturá-los no guia de APIs REST em Go. Para entender mais sobre generics e como eles podem ajudar a criar fuzz helpers genéricos, confira nosso guia prático.
Quer ver como outras linguagens abordam testes? Confira como Rust lida com testes e segurança de memória em tempo de compilação, ou como Python usa frameworks como Hypothesis para property-based testing.