Desde Go 1.18, el toolchain incluye un motor de fuzzing nativo que genera automáticamente miles de inputs mutados para encontrar crashes, panics y comportamientos inesperados. A diferencia de los tests unitarios, el fuzzer explora el espacio de entradas sin que tengas que pensar en casos extremos. Es especialmente eficaz en parsers, deserializadores y funciones que procesan datos externos.
Sintaxis básica: FuzzXxx, f.Add y f.Fuzz
package parserfecha_test
import (
"testing"
"time"
)
func FuzzParseFecha(f *testing.F) {
// Corpus inicial: ejemplos válidos y casos borde
f.Add("2024-01-15")
f.Add("2000-02-29") // año bisiesto
f.Add("")
f.Add("no-es-una-fecha")
// El fuzzer muta estos seeds y llama a esta función con cada variante
f.Fuzz(func(t *testing.T, input string) {
// La función no debe entrar en pánico
fecha, err := ParseFecha(input)
if err != nil {
return // error esperado para entradas inválidas
}
// Propiedad: si parseó bien, debe re-serializar igual
if fecha.Format("2006-01-02") != input {
t.Errorf("ida y vuelta fallida: %q ? %v ? %q",
input, fecha, fecha.Format("2006-01-02"))
}
})
}
func ParseFecha(s string) (time.Time, error) {
return time.Parse("2006-01-02", s)
}
Ejecutar el fuzzer
// Solo ejecutar los seeds del corpus (igual que go test normal) go test -run FuzzParseFecha // Fuzzing real: genera inputs durante 30 segundos go test -fuzz=FuzzParseFecha -fuzztime=30s // Fuzzing hasta encontrar un fallo o interrumpir manualmente go test -fuzz=FuzzParseFecha
Cuando el fuzzer encuentra un fallo, guarda el input que lo provocó en testdata/fuzz/FuzzParseFecha/. A partir de ese momento, go test (sin -fuzz) reproduce ese caso automáticamente en cada ejecución.
Ejemplo 2: fuzzing de un parser CSV
func FuzzParseCSV(f *testing.F) {
f.Add("a,b,cn1,2,3n")
f.Add("")
f.Add("campo con "comillas",otro")
f.Fuzz(func(t *testing.T, csv string) {
records, err := ParseCSV(strings.NewReader(csv))
if err != nil {
return
}
// Propiedad: todos los registros deben tener el mismo número de columnas
if len(records) > 1 {
cols := len(records[0])
for i, row := range records[1:] {
if len(row) != cols {
t.Errorf("fila %d tiene %d columnas, esperadas %d", i+1, len(row), cols)
}
}
}
})
}
Ejemplo 3: invariantes de un tipo Moneda
func FuzzMoneda(f *testing.F) {
f.Add("10.50")
f.Add("0.00")
f.Add("-5.99")
f.Add("9999999.99")
f.Fuzz(func(t *testing.T, s string) {
m, err := ParseMoneda(s)
if err != nil {
return
}
// Invariante: la moneda no puede tener más de 2 decimales
texto := m.String()
partes := strings.Split(texto, ".")
if len(partes) == 2 && len(partes[1]) > 2 {
t.Errorf("moneda %q tiene más de 2 decimales: %q", s, texto)
}
})
}
Ejemplo 4: fuzzing de JSON round-trip
func FuzzJSONRoundTrip(f *testing.F) {
f.Add(`{"nombre":"Ana","edad":30}`)
f.Add(`{}`)
f.Add(`{"valor":1e308}`)
f.Fuzz(func(t *testing.T, input string) {
var m map[string]any
if err := json.Unmarshal([]byte(input), &m); err != nil {
return // input no es JSON válido
}
// Re-serializar y volver a deserializar
data, err := json.Marshal(m)
if err != nil {
t.Fatalf("marshal falló con input válido: %v", err)
}
var m2 map[string]any
if err := json.Unmarshal(data, &m2); err != nil {
t.Fatalf("unmarshal del resultado falló: %v", err)
}
})
}
Gestión del corpus
Los inputs que encuentra el fuzzer se guardan en testdata/fuzz/NombreFuzz/ como ficheros de texto. Añade tus propios casos al corpus para guiar la exploración:
// testdata/fuzz/FuzzParseCSV/caso_especial
go test corpus v1
string("a,,brn")
El corpus de la comunidad de Go (https://storage.googleapis.com/go-fuzzcache/) también acumula casos encontrados por otros desarrolladores sobre los paquetes estándar. Si trabajas con parsers propios, ejecutar el fuzzer durante horas en CI puede descubrir vulnerabilidades antes de que lleguen a producción.
