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
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:
- Slice van een anonieme structuur met de gebruikelijke velden:
name, de invoer, de verwachte uitvoer (en misschien een verwachte fout). for _, tc := range cases— de uitvoeringslus.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 (tenzijt.Fatalwordt gebruikt). - Opdrachtregelfiltering:
go test -run TestAbs/negvoert alleen dat geval uit. - Optioneel parallellisme: roep
t.Parallel()binnen de func aan om te parallelliseren.
go test -v -run TestAbs/negGevallen met verwachte fouten
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:
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
Definieer een cases-segment van een anonieme structuur met velden in en want, gevuld met 3 cases om Abs te testen (positief, negatief, nul).
Oplossing beschikbaar na 3 pogingen
Voeg t.Run toe met tc.name om van elke case een benoemde subtest te maken.
Oplossing beschikbaar na 3 pogingen
Wat is het belangrijkste voordeel van het tabelgestuurde patroon ten opzichte van N afzonderlijke testfuncties?
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) { ... })
}