Lekcje modułu (2/5)
Testy sterowane tabelami
Testy oparte na tabelach to idiomatyczny wzorzec Go: zamiast pisać N oddzielnych funkcji TestThisSpecificCase, deklarujesz wycinek przypadków i iterujesz. Jest JEDEN blok asercji, a dane testowe są oddzielone od logiki.
Wzór podstawowy
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)
}
})
}
}Trzy składniki:
- Kawałek anonimowej struktury ze zwykłymi polami:
name, wejściami, oczekiwanym wyjściem (i być może oczekiwanym błędem). for _, tc := range cases— pętla wykonawcza.t.Run(tc.name, func(t *testing.T) { ... })— każdy przypadek staje się podtestem mającym swoją nazwę.
Dlaczego t.Run?
t.Run tworzy podtest, który ma:
- Indywidualna nazwa pokazana na wyjściu:
TestAbs/pos,TestAbs/neg, ... - Własny
t *testing.T: awaria w jednym przypadku nie powstrzymuje pozostałych (chyba że zostanie użytyt.Fatal). - Filtrowanie wiersza poleceń:
go test -run TestAbs/neguruchamia tylko ten przypadek. - Opcjonalna równoległość: wywołaj
t.Parallel()wewnątrz funkcji, aby zrównoleglić.
go test -v -run TestAbs/negPrzypadki z oczekiwanymi błędami
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)
}
})
}Pułapka związana z zamknięciem pętli (przed wersją 1.22)
W wersjach wcześniejszych niż Go 1.22 zmienna tc była współdzielona w iteracjach: wywołanie t.Parallel() w t.Run powodowało, że wszystkie podtesty były uruchamiane w tym samym ostatnim przypadku. Rozwiązaniem było:
for _, tc := range cases {
tc := tc // local rebind (pre-1.22)
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// ...
})
}Nazywanie przypadków
Najlepsze praktyki:
- Krótka, ale sugestywna nazwa:
"empty input","too large","negative". - Brak spacji, jeśli chcesz łatwo filtrować za pomocą
-run(lub użyj podkreśleń). - Zawsze uwzględniaj przypadek „szczęśliwej ścieżki”, przypadek „krawędziowy” (zero, pusty) i co najmniej jeden przypadek błędu.
Ćwiczenia
Zdefiniuj wycinek przypadków anonimowej struktury z polami in i want, wypełniony 3 przypadkami do przetestowania Abs (dodatni, ujemny, zero).
Rozwiązanie dostępne po 3 próbach
Dodaj t.Run z tc.name, aby zamienić każdy przypadek w nazwany podtest.
Rozwiązanie dostępne po 3 próbach
Jaka jest główna zaleta wzorca opartego na tabeli w porównaniu z N oddzielnymi funkcjami testowymi?
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) { ... })
}