Pregunta y escenarios adecuados
Mantienes un analizador sintáctico (parser) de entrada en Go que ocasionalmente recibe Unicode, bytes truncados o longitudes extremas en producción. Explica cómo diseñar un objetivo de fuzzing testing.F, elegir semillas, expresar propiedades cuando la salida exacta no se puede conocer y preservar una falla como una prueba de regresión después de la corrección.
Esto se adapta a roles de backend, infraestructura y herramientas de prueba (test-tooling). La guía de entrevistas técnicas de Microsoft evalúa explícitamente las pruebas, los límites y las implicaciones de seguridad; la guía para SDE de Amazon incluye la programación y temas centrales de software. La señal que busca la entrevista es el ciclo de ingeniería, no la memorización de un comando.
Qué está evaluando el entrevistador
- Distinguir los valores esperados de una prueba de ejemplo frente a las propiedades de una prueba de fuzzing.
- Elegir un objetivo rápido y sin efectos secundarios externos.
- Explicar el corpus de semillas, la guía por cobertura, la minimización y la reproducción (replay).
- Manejar UTF-8 inválido, entradas vacías, entradas de tamaño excesivo y límites de recursos.
- Conectar el descubrimiento, el diagnóstico, la reparación, la reejecución y el corpus confirmado (committed).
Una respuesta débil dice "generar entradas aleatorias". Una respuesta sólida nombra el invariante, los comandos, la ruta de la falla y la división en CI.
Aclaraciones antes de responder
- ¿La entrada del parser es un
string,[]byteo varios campos? Esto determina los argumentos de fuzz y la codificación del corpus. - ¿Qué entradas inválidas representan errores esperados? Un contrato de errores cambia la aserción.
- ¿Cuál es el presupuesto de recursos por llamada? Una entrada lenta puede ser una señal de denegación de servicio.
- ¿Buscamos panics, errores semánticos o regresiones de compatibilidad? Cada uno necesita propiedades y semillas diferentes.
Estructura de respuesta de 30 segundos
Comienzo con una propiedad verificable, como "la codificación después del parseo preserva la misma estructura" o "una entrada inválida devuelve un error controlado". Agrego casos de borde reales con f.Add y luego mantengo f.Fuzz rápido, determinista y acotado. Un go test ordinario ejecuta las semillas y las fallas guardadas; un trabajo de CI independiente ejecuta go test -fuzz con un límite de tiempo. Go minimiza una entrada fallida en testdata/fuzz/<name>. Después de corregir la causa raíz, reproduzco esa entrada, ejecuto toda la suite de pruebas y confirmo (commit) el corpus para que el descubrimiento se convierta en una restricción de regresión.
Respuesta detallada paso a paso
1. Comenzar con una propiedad, no con una respuesta esperada
Para un parser, utiliza una propiedad de ida y vuelta (round-trip): Encode(Parse(x)) debe representar x después de la normalización. Para una transformación de strings, usa idempotencia, preservación de longitud o validez de UTF-8. Permite la normalización especificada; no confundas la disposición de bytes con el contrato.
2. Construir un objetivo de fuzzing acotado
func FuzzParseRoundTrip(f *testing.F) {
f.Add([]byte("name=alice"))
f.Add([]byte{})
f.Fuzz(func(t *testing.T, input []byte) {
t.Helper()
if len(input) > 1<<20 {
t.Skip()
}
got, err := Parse(input)
if err != nil {
return
}
again, err := Parse(Encode(got))
if err != nil || !Equal(got, again) {
t.Fatalf("round trip failed: %v", err)
}
})
}Los tipos y el orden de f.Add deben coincidir con la función de devolución de llamada (callback). El objetivo no debe escribir archivos, llamar a la red ni depender del tiempo; de lo contrario, el fuzzing en paralelo produce fallas no reproducibles.
3. Sembrar límites de negocio
Las semillas deben cubrir versiones del protocolo, valores vacíos, campos duplicados, texto que no sea ASCII, truncamiento y muestras saneadas de producción. Go ejecuta las semillas durante las pruebas ordinarias, por lo que cada semilla debe ser ligera y estable.
4. Dividir los modos de ejecución
Antes de un commit, ejecuta go test -run=FuzzParseRoundTrip para verificar las semillas. Un trabajo dedicado puede ejecutar go test -fuzz=FuzzParseRoundTrip -fuzztime=10s. La bandera de tiempo limita la exploración; CI gestiona el paralelismo y los presupuestos por paquete en lugar del código de producción.
5. Reproducir y diagnosticar la entrada minimizada
Go minimiza una entrada que aún reproduce la falla y la escribe en testdata/fuzz/FuzzParseRoundTrip/. Reproduce con go test -run=FuzzParseRoundTrip/<id> y luego clasifica la causa raíz: aserción, panic, agotamiento de recursos o no determinismo de la prueba. La muestra debe explicar el error, no ser un blob opaco.
6. Hacer que la corrección sea permanente
Reproduce la falla después de la corrección y luego ejecuta todos los go test. Las fallas guardadas también se ejecutan sin -fuzz, así que revisa el corpus en busca de secretos, redacción de datos y límites de tamaño antes de confirmarlo en el repositorio.
7. Medir la calidad del fuzzing
Si la cobertura deja de crecer, agrega semillas estructuradas o mejora la generación en lugar de solo aumentar la duración. Las fallas frecuentes por tiempo de espera (timeout) requieren entradas más pequeñas o el aislamiento de rutas costosas; trátalas como defectos de rendimiento. Una tasa de descubrimiento a la baja no es prueba de corrección.
Ejemplo de respuesta de alta calidad
Defino el fuzzing en torno a una propiedad reproducible, no a un resultado de negocio esperado para cada entrada aleatoria. Para un parser, la entrada válida debe parsearse y volver a codificarse en la misma estructura; la entrada inválida debe devolver un error controlado y nunca entrar en panic. Siembro entradas vacías, el tamaño máximo aceptado, campos duplicados, Unicode y casos de producción saneados. El objetivo está acotado y libre de efectos secundarios. Los desarrolladores ejecutan go test -run=FuzzX; CI explora con un -fuzztime fijo. Cuando Go escribe una falla minimizada, la reproduzco, identifico la causa raíz, corrijo la implementación, ejecuto las pruebas ordinarias y el fuzzing, y hago commit de testdata/fuzz/FuzzX. De este modo, un descubrimiento se convierte en una verificación de regresión en cada cambio.
Errores comunes
- Tratar el fuzzing como una carga aleatoria → no existe un oráculo → define primero un invariante y un contrato de errores.
- Llamar a un servicio en vivo desde el objetivo → la red y el estado hacen que las fallas sean intermitentes (flaky) → utiliza un fake determinista.
- Ejecutar fallas guardadas únicamente en modo fuzz → las correcciones pueden sufrir regresiones → consérvalas en
testdata/fuzz. - Aceptar entradas no acotadas → un solo caso consume el presupuesto → impón un límite y registra las omisiones.
- Hacer commit de cada caso generado → ruido y crecimiento del repositorio → conserva los casos que reproducen errores o cubren ramas clave.
Preguntas de seguimiento y respuestas
¿Es una aserción de ida y vuelta (round-trip) demasiado estricta cuando el parseo normaliza la entrada?
Sí. Compara árboles de sintaxis abstracta (AST) normalizados o conjuntos de campos y especifica qué diferencias de ordenamiento, espacios en blanco o mayúsculas/minúsculas se ignoran intencionalmente.
¿Se puede hacer commit de una muestra fallida que contenga secretos de usuario?
No. Revisa credenciales y datos personales, redacta y verifica que la muestra redactada siga fallando. Si no se puede confirmar de forma segura, mantén la reproducción interna controlada y envía un equivalente sintético.
El presupuesto de CI es de cinco minutos. ¿Cómo debería encajar el fuzzing?
Ejecuta las semillas y las fallas conocidas en cada build. Limita la exploración por tiempo, paquetes y recursos; registra los tiempos de espera en lugar de omitirlos silenciosamente.
¿Cómo saber si la prueba de fuzzing está rota?
Reproduce el caso minimizado e inspecciona el estado global, la aleatoriedad y la temporización de las goroutines. Luego escribe una prueba unitaria determinista; si es intermitente, corrige el aislamiento antes de tocar el código de producción.
¿Pueden múltiples objetivos compartir un corpus?
Comparte la lógica de conversión solo cuando el formato de entrada y la semántica coincidan. Mantén directorios e invariantes separados para que el corpus de un objetivo no pueda ocultar una brecha de cobertura de otro.