Lecciones del módulo (2/5)
Pruebas basadas en tablas
Las pruebas basadas en tablas son el patrón idiomático de Go: en lugar de escribir N funciones TestThisSpecificCase separadas, se declara una porción de casos y se itera. Hay UN bloque de aserción y los datos de prueba están separados de la lógica.
El patrón base
func TestAbs(t *testing.T) {
cases := []struct {
name string
in int
want int
}{
{"pos", 3, 3},
{"neg", -3, 3},
{"zero", 0, 0},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := Abs(tc.in); got != tc.want {
t.Errorf("Abs(%d) = %d; voglio %d", tc.in, got, tc.want)
}
})
}
}Tres ingredientes:
- Porción de estructura anónima con los campos habituales:
name, las entradas, la salida esperada (y tal vez un error esperado). for _, tc := range cases: el ciclo de ejecución.t.Run(tc.name, func(t *testing.T) { ... })— cada caso se convierte en una subprueba con su propio nombre.
¿Por qué t.Run?
t.Run crea una subprueba que tiene:
- Nombre individual mostrado en la salida:
TestAbs/pos,TestAbs/neg, ... - Su propio
t *testing.T: un fallo en un caso no detiene los demás (a menos que se utilicet.Fatal). - Filtrado de línea de comando:
go test -run TestAbs/negejecuta solo ese caso. - Paralelismo opcional: llame a
t.Parallel()dentro de la función para paralelizar.
go test -v -run TestAbs/negCasos con errores esperados
cases := []struct {
name string
in string
want int
wantErr bool
}{
{"ok", "42", 42, false},
{"bad", "x", 0, true},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got, err := Atoi(tc.in)
if (err != nil) != tc.wantErr {
t.Fatalf("err = %v, wantErr = %v", err, tc.wantErr)
}
if !tc.wantErr && got != tc.want {
t.Errorf("got %d, want %d", got, tc.want)
}
})
}El problema del cierre del bucle (antes de Go 1.22)
En versiones anteriores a Go 1.22, la variable tc se compartió entre iteraciones: llamar a t.Parallel() dentro de t.Run provocaba que todas las subpruebas se ejecutaran en el mismo último caso. La solución fue:
for _, tc := range cases {
tc := tc // local rebind (pre-1.22)
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// ...
})
}Nombrar los casos
Mejores prácticas:
- Un nombre corto pero evocador:
"empty input","too large","negative". - Sin espacios si quieres filtrar fácilmente con
-run(o usar guiones bajos). - Incluya siempre un caso de "camino feliz", un caso de "borde" (cero, vacío) y al menos un caso de error.
Ejercicios
Defina una porción de casos de estructura anónima con campos en y deseo, completa con 3 casos para probar Abs (positivo, negativo, cero).
Solución disponible después de 3 intentos
Agregue t.Run con tc.name para convertir cada caso en una subprueba con nombre.
Solución disponible después de 3 intentos
¿Cuál es la principal ventaja del patrón basado en tablas sobre N funciones de prueba separadas?
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) { ... })
}