Un table-driven test en Go es un test que define sus casos en una tabla y los recorre con un bucle.
Es una técnica muy usada en Go porque encaja perfectamente con la forma simple y explícita del lenguaje. En vez de copiar y pegar cinco tests casi iguales, definimos los datos de entrada, el resultado esperado y dejamos que el bucle haga el trabajo aburrido.
Vamos a verlo con un ejemplo pequeño, porque asi se entiende enseguida.
El problema de repetir tests
Tenemos esta función:
package calculadora
func EsPar(n int) bool {
return n%2 == 0
}Podríamos escribir varios tests separados:
func TestEsParConDos(t *testing.T) {
if !EsPar(2) {
t.Error("2 debería ser par")
}
}
func TestEsParConTres(t *testing.T) {
if EsPar(3) {
t.Error("3 no debería ser par")
}
}Funciona, pero empieza a oler a repetición. Y la repetición en tests también se paga.
Crear la tabla de casos
La idea es definir una lista de casos:
func TestEsPar(t *testing.T) {
casos := []struct {
nombre string
entrada int
esperado bool
}{
{nombre: "dos es par", entrada: 2, esperado: true},
{nombre: "tres es impar", entrada: 3, esperado: false},
{nombre: "cero es par", entrada: 0, esperado: true},
{nombre: "negativo par", entrada: -4, esperado: true},
}
for _, caso := range casos {
resultado := EsPar(caso.entrada)
if resultado != caso.esperado {
t.Errorf("%s: EsPar(%d) = %v; esperado %v",
caso.nombre,
caso.entrada,
resultado,
caso.esperado,
)
}
}
}Ahora añadir un caso nuevo es tan sencillo como añadir una línea a la tabla.
Subtests con t.Run
Podemos mejorar el resultado usando t.Run, que crea un subtest por cada caso.
func TestEsPar(t *testing.T) {
casos := []struct {
nombre string
entrada int
esperado bool
}{
{"dos es par", 2, true},
{"tres es impar", 3, false},
{"cero es par", 0, true},
}
for _, caso := range casos {
t.Run(caso.nombre, func(t *testing.T) {
resultado := EsPar(caso.entrada)
if resultado != caso.esperado {
t.Errorf("EsPar(%d) = %v; esperado %v",
caso.entrada,
resultado,
caso.esperado,
)
}
})
}
}Esto hace que la salida de go test sea más clara, porque cada caso aparece con su propio nombre.
Ejecutar un subtest concreto
Podemos ejecutar un subtest por nombre:
go test -run TestEsPar/dosEsto es muy cómodo cuando tenéis una tabla grande y solo falla un caso. No hace falta ejecutar todo el paquete una y otra vez.
Cuidado con las variables del bucle
Desde Go 1.22, cada iteración declarada por el range recibe su propia variable. En módulos con una versión anterior, un subtest paralelo podía capturar la misma variable en todas las vueltas; este era el patrón defensivo:
for _, caso := range casos {
caso := caso
t.Run(caso.nombre, func(t *testing.T) {
resultado := EsPar(caso.entrada)
if resultado != caso.esperado {
t.Errorf("resultado incorrecto")
}
})
}Ese caso := caso crea una variable nueva para cada vuelta. No hace falta en módulos con semántica de Go 1.22 o posterior, aunque aparecerá con frecuencia en código antiguo.
Casos con errores
También podemos probar funciones que devuelven errores.
func TestDividir(t *testing.T) {
casos := []struct {
nombre string
a, b int
esperado int
esperaError bool
}{
{"division normal", 10, 2, 5, false},
{"division por cero", 10, 0, 0, true},
}
for _, caso := range casos {
t.Run(caso.nombre, func(t *testing.T) {
resultado, err := Dividir(caso.a, caso.b)
if caso.esperaError && err == nil {
t.Fatal("se esperaba error")
}
if caso.esperaError {
return
}
if err != nil {
t.Fatalf("no se esperaba error: %v", err)
}
if resultado != caso.esperado {
t.Errorf("resultado = %d; esperado %d", resultado, caso.esperado)
}
})
}
}