Lezioni del modulo (2/5)
Table-driven test
I table-driven test sono il pattern idiomatico di Go: invece di scrivere N funzioni TestQuestoCasoSpecifico, dichiari una slice di casi e iteri. Il codice di asserzione è UNO solo, i dati di test sono separati dalla logica.
Lo schema 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)
}
})
}
}Tre ingredienti:
- Slice di struct anonima con campi tipici:
name, gli input, l'output atteso (e magari un error atteso). for _, tc := range cases— il loop di esecuzione.t.Run(tc.name, func(t *testing.T) { ... })— ogni caso diventa un sub-test con nome proprio.
Perché t.Run?
t.Run crea un sub-test che ha:
- Nome individuale mostrato nell'output:
TestAbs/pos,TestAbs/neg, ... t *testing.Tproprio: un fallimento in un caso non blocca gli altri (a meno dit.Fatal).- Filtro a riga di comando:
go test -run TestAbs/negper eseguire solo quel caso. - Parallelismo opzionale: chiama
t.Parallel()dentro la func per parallelizzare.
go test -v -run TestAbs/negCasi con errore atteso
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)
}
})
}Trabocchetto della closure del loop (pre-Go 1.22)
In versioni precedenti a Go 1.22, la variabile tc era condivisa fra le iterazioni: chiamare t.Parallel() dentro t.Run faceva eseguire tutti i sub-test sullo stesso ultimo caso. La soluzione era:
for _, tc := range cases {
tc := tc // rebind locale (pre-1.22)
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// ...
})
}Naming dei casi
Le best practice:
- Nome breve ma evocativo:
"empty input","too large","negative". - Niente spazi se vuoi filtrare facilmente con
-run(oppure usa underscore). - Includi sempre un caso "happy path", un caso "edge" (zero, vuoto) e almeno un caso d'errore.
Esercizi
Definisci uno slice cases di struct anonima con campi in e want, popolato con 3 casi per testare Abs (positivo, negativo, zero).
Soluzione disponibile dopo 3 tentativi
Aggiungi t.Run con tc.name per trasformare ogni caso in un sub-test nominato.
Soluzione disponibile dopo 3 tentativi
Qual è il principale vantaggio del pattern table-driven rispetto a N funzioni Test separate?
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) { ... })
}