← Voltar para o blog

Fuzzing em Go: Encontre Bugs Automaticamente com Testes Nativos

Aprenda fuzzing em Go com testes nativos: escreva fuzz tests, use seed corpus, encontre bugs em parsers e validações, e integre fuzzing no CI/CD do seu projeto.

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, []byte
  • int, int8, int16, int32, int64
  • uint, uint8, uint16, uint32, uint64
  • float32, float64
  • bool

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

AbordagemQuando Usar
Table-driven testsCenários conhecidos, edge cases documentados
FuzzingEncontrar bugs desconhecidos, testar robustez
Property-based testsVerificar 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

  1. Mantenha fuzz targets rápidos — evite I/O, rede e operações caras dentro do f.Fuzz()
  2. 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)
  3. Teste propriedades, não valores específicos — fuzzing é sobre invariantes (idempotência, roundtrip, ausência de panic)
  4. Use t.Skip() para inputs inválidos conhecidos — quando certos inputs não fazem sentido para o domínio
  5. Combine com tratamento de erros robusto — funções que retornam error adequadamente 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.