Cuando las pruebas unitarias mienten y los errores persisten
Escribiste 20 pruebas unitarias. Todas en verde. El CI pasa. Estás contento. Y luego — un error en producción. ¿Te suena? El escenario clásico: solo probaste los casos que se te ocurrieron. Y la realidad, como para fastidiar, presenta combinaciones que ni siquiera imaginaste.
En Go existe un enfoque potente pero subestimado: pruebas basadas en propiedades (o property-based testing). En lugar de verificar entradas y salidas específicas, describes las propiedades que debe cumplir una función, y el framework genera cientos de datos de prueba aleatorios. Si se viola una propiedad — has encontrado un error.
En este artículo, veremos cómo implementar pruebas basadas en propiedades en Go con la biblioteca gopter, escribiremos ejemplos reales y observaremos qué errores se pueden detectar. ¡Allá vamos!
¿Qué son las pruebas basadas en propiedades y por qué son necesarias?
Imagina que pruebas una función de ordenamiento. Una prueba unitaria típica:
func TestSort(t *testing.T) { input := []int{3, 1, 2} result := sort(input) expected := []int{1, 2, 3} if !reflect.DeepEqual(result, expected) { t.Errorf("got %v, want %v", result, expected) }}Bien, funciona. Pero ¿qué pasa si el array está vacío? ¿Y si todos los elementos son iguales? ¿Y si hay números negativos? Puedes escribir 10 pruebas más, pero igual algo se te escapará.
Las pruebas basadas en propiedades resuelven este problema de raíz: formulas invariantes — propiedades que deben ser verdaderas para cualquier entrada válida. Por ejemplo, para el ordenamiento:
- La longitud del array ordenado es igual a la del array original.
- Cada elemento del array original está presente en el ordenado (y viceversa).
- El array está ordenado de forma ascendente.
El framework (por ejemplo, gopter o el integrado testing/quick) genera datos de entrada aleatorios y verifica que todas las propiedades se cumplan. Si hay un error — te enterarás, incluso sin saber dónde buscar.
Bibliotecas de Go para pruebas basadas en propiedades
En el ecosistema de Go hay varias herramientas:
testing/quick— paquete integrado, minimalista, pero con capacidades limitadas.gopter— biblioteca potente con generadores, reducción (minimización de datos de entrada) y configuraciones flexibles. Recomendamos esta.rapid— alternativa moderna, más eficiente.
Usaremos gopter por su mejor documentación y comunidad. Instalación:
go get github.com/leanovate/gopterEscribimos nuestra primera prueba basada en propiedades en Go
Supongamos que tenemos una función Reverse que invierte una cadena:
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)}¿Qué propiedad podemos verificar? Si invertimos la cadena dos veces, deberíamos obtener la original:
import ( "testing" "github.com/leanovate/gopter" "github.com/leanovate/gopter/gen")
func TestReverseTwice(t *testing.T) { properties := gopter.NewProperties(nil)
properties.Property("reverse(reverse(s)) == s", prop.ForAll( func(s string) bool { return Reverse(Reverse(s)) == s }, gen.AnyString(), ))
properties.TestingRun(t)}Ejecuta la prueba — y gopter generará 100 cadenas aleatorias, verificando la propiedad. Si todo está bien — la prueba estará en verde. Si no — obtendrás un contraejemplo (la cadena más pequeña con la que falló la prueba).