Vai al contenuto
eLearner.app
Modulo 9 · Lezione 2 di 542/50 nel corso~14 min
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

Go
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:

  1. Slice di struct anonima con campi tipici: name, gli input, l'output atteso (e magari un error atteso).
  2. for _, tc := range cases — il loop di esecuzione.
  3. 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.T proprio: un fallimento in un caso non blocca gli altri (a meno di t.Fatal).
  • Filtro a riga di comando: go test -run TestAbs/neg per eseguire solo quel caso.
  • Parallelismo opzionale: chiama t.Parallel() dentro la func per parallelizzare.
Bash
go test -v -run TestAbs/neg

Casi con errore atteso

Go
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:

Go
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

Esercizio#go.m9.l2.e1
Tentativi: 0Caricamento…

Definisci uno slice cases di struct anonima con campi in e want, popolato con 3 casi per testare Abs (positivo, negativo, zero).

Caricamento editor…

Soluzione disponibile dopo 3 tentativi

Esercizio#go.m9.l2.e2
Tentativi: 0Caricamento…

Aggiungi t.Run con tc.name per trasformare ogni caso in un sub-test nominato.

Caricamento editor…

Soluzione disponibile dopo 3 tentativi

Quiz#go.m9.l2.e3
Pronto

Qual è il principale vantaggio del pattern table-driven rispetto a N funzioni Test separate?

Go
for _, tc := range cases {
  t.Run(tc.name, func(t *testing.T) { ... })
}
Opzioni di risposta