Direct naar de hoofdinhoud
Module 9 · Les 2 van 542/50 in de cursus~14 min
Modulelessen (2/5)

Tabelgestuurde tests

Tabelgestuurde tests zijn het idiomatische patroon van Go: in plaats van N afzonderlijke TestThisSpecificCase functies te schrijven, declareert u een aantal cases en herhaalt u deze. Er is EEN beweringsblok en de testgegevens zijn gescheiden van de logica.

Het basispatroon

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)
            }
        })
    }
}

Drie ingrediënten:

  1. Slice van een anonieme structuur met de gebruikelijke velden: name, de invoer, de verwachte uitvoer (en misschien een verwachte fout).
  2. for _, tc := range cases — de uitvoeringslus.
  3. t.Run(tc.name, func(t *testing.T) { ... }) — elke casus wordt een subtest met een eigen naam.

Waarom t.Run?

t.Run maakt een subtest met:

  • Individuele naam weergegeven in de uitvoer: TestAbs/pos, TestAbs/neg, ...
  • Zijn eigen t *testing.T: een fout in het ene geval stopt de andere niet (tenzij t.Fatal wordt gebruikt).
  • Opdrachtregelfiltering: go test -run TestAbs/neg voert alleen dat geval uit.
  • Optioneel parallellisme: roep t.Parallel() binnen de func aan om te parallelliseren.
Bash
go test -v -run TestAbs/neg

Gevallen met verwachte fouten

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)
        }
    })
}

De valkuil van de lussluiting (vóór Go 1.22)

In versies vóór Go 1.22 werd de variabele tc gedeeld over iteraties: het aanroepen van t.Parallel() binnen t.Run zorgde ervoor dat alle subtests tegen hetzelfde laatste geval werden uitgevoerd. De oplossing was:

Go
for _, tc := range cases {
    tc := tc // local rebind (pre-1.22)
    t.Run(tc.name, func(t *testing.T) {
        t.Parallel()
        // ...
    })
}

Het benoemen van de gevallen

Beste praktijken:

  • Een korte maar suggestieve naam: "empty input", "too large", "negative".
  • Geen spaties als u eenvoudig wilt filteren met -run (of onderstrepingstekens wilt gebruiken).
  • Neem altijd een 'happy path'-geval, een 'edge'-geval (nul, leeg) en ten minste één foutgeval op.

Oefeningen

Oefening#go.m9.l2.e1
Pogingen: 0Laden…

Definieer een cases-segment van een anonieme structuur met velden in en want, gevuld met 3 cases om Abs te testen (positief, negatief, nul).

Editor laden…

Oplossing beschikbaar na 3 pogingen

Oefening#go.m9.l2.e2
Pogingen: 0Laden…

Voeg t.Run toe met tc.name om van elke case een benoemde subtest te maken.

Editor laden…

Oplossing beschikbaar na 3 pogingen

Quiz#go.m9.l2.e3
Klaar

Wat is het belangrijkste voordeel van het tabelgestuurde patroon ten opzichte van N afzonderlijke testfuncties?

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