Para asegurar que tu código no solo sea correcto, sino también eficiente y robusto, necesitas herramientas que vayan más allá de los tests unitarios tradicionales. Los benchmarks miden la velocidad y el uso de memoria de tus funciones; los ejemplos (ExampleXxx) actúan como documentación viva que se verifica automáticamente; y el fuzzing [disponible desde Go 1.18] busca errores de lógica o crashes inyectando datos aleatorios de forma masiva.
Los benchmarks funcionan mediante un bucle controlado por el runtime de Go. Usas testing.B y un bucle for i := 0; i < b.N; i++, donde b.N no es un número fijo, sino que el motor de go test lo aumenta dinámicamente hasta que la ejecución sea lo suficientemente larga para ser estadísticamente significativa. Si no usas b.N y pones un número fijo, tus mediciones carecerán de sentido. Los ejemplos usan un comentario especial, // Output:, para que el compilador compare la salida estándar de la función con lo que tú esperas; si no coinciden, el test falla. Por último, el fuzzing es fundamental cuando procesas datos externos (como JSON o protocolos binarios): en lugar de probar tres casos conocidos, dejas que Go genere miles de variaciones para encontrar ese caracter Unicode extraño que rompe tu lógica. Si ignoras el fuzzing en funciones críticas, estás dejando la puerta abierta a ataques de denegación de servicio (DoS) o errores de desbordamiento.
package main_test
import (
"fmt"
"testing"
)
// Reverse invierte una cadena de runes.
// Nota: En una implementación real, esta función podría fallar con ciertos
// caracteres Unicode combinados, lo cual es un excelente caso de uso para Fuzzing.
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)
}
// BenchmarkReverse mide el rendimiento de la función Reverse.
func BenchmarkReverse(b *testing.B) {
// Setup: preparamos el dato fuera del loop de medición.
input := "esto es una cadena larga para el benchmark"
// ResetTimer excluye el tiempo de preparación del cronómetro.
b.ResetTimer()
for i := 0; i < b.N; i++ {
Reverse(input)
}
}
// ExampleReverse documenta el comportamiento esperado en la documentación oficial.
func ExampleReverse() {
fmt.Println(Reverse("Go"))
// Output: oG
}
// FuzzReverse busca errores de consistencia mediante datos aleatorios.
func FuzzReverse(f *testing.F) {
// Añadimos "seeds" (semillas) para darle pistas al fuzzer.
f.Add("hello")
f.Add("世界")
f.Add("12345")
f.Fuzz(func(t *testing.T, input string) {
reversed := Reverse(input)
backAgain := Reverse(reversed)
// Propiedad: Revertir una cadena dos veces debería devolver la original.
if input != backAgain {
t.Errorf("No se pudo recuperar la cadena original: %q -> %q -> %q", input, reversed, backAgain)
}
})
}
Desglose del código
En el BenchmarkReverse, fíjate en b.ResetTimer(). Es vital porque si el proceso de preparación de input fuera pesado (por ejemplo, leer un archivo), ese tiempo se sumaría al tiempo de ejecución de la función, arruinando la métrica. Al llamar a b.ResetTimer(), reiniciamos el cronómetro justo antes de entrar al bucle for i := 0; i < b.N; i++. Para ejecutar esto viendo la memoria, debes usar go test -bench=. -benchmem.
El ExampleReverse es un caso de uso de documentación ejecutable. Al usar fmt.Println y el comentario // Output: oG, Go garantiza que el ejemplo que el usuario lee en la documentación es exactamente lo que el código hace. Si alguien cambia la lógica de Reverse y el output cambia, el test fallará.
En FuzzReverse, el método f.Add inicializa el corpus. El motor de fuzzing tomará esos valores (“hello”, “世界”) y los mutará (cambiará bytes, añadirá caracteres nulos, etc.) para intentar romper la propiedad input == backAgain. Si encuentra una combinación de caracteres que hace que Reverse(Reverse(s)) no sea igual a s, el fuzzer fallará y te mostrará la entrada exacta que causó el error.
El error frecuente
Un error clásico en los benchmarks es olvidar el bucle for i := 0; i < b.N; i++ o usar un número fijo como for i := 0; i < 1000; i++.
// MAL: Esto no es un benchmark real
func BenchmarkWrong(b *testing.B) {
for i := 0; i < 1000; i++ {
Reverse("data")
}
}
Si haces esto, el test de Go no puede ajustar la cantidad de iteraciones para obtener una medición estable y el resultado será un ruido estadístico sin valor. El motor de testing necesita controlar b.N para decidir si debe ejecutar la función 10, 1000 o 1,000,000 de veces para obtener una precisión fiable.
N° 154