Przejdź do głównej treści
eLearner.app
Moduł 9 · Lekcja 2 z 542/50 w kursie~14 min
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

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

Trzy składniki:

  1. Kawałek anonimowej struktury ze zwykłymi polami: name, wejściami, oczekiwanym wyjściem (i być może oczekiwanym błędem).
  2. for _, tc := range cases — pętla wykonawcza.
  3. 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żyty t.Fatal).
  • Filtrowanie wiersza poleceń: go test -run TestAbs/neg uruchamia tylko ten przypadek.
  • Opcjonalna równoległość: wywołaj t.Parallel() wewnątrz funkcji, aby zrównoleglić.
Bash
go test -v -run TestAbs/neg

Przypadki z oczekiwanymi błędami

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

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:

Go
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

Ćwiczenie#go.m9.l2.e1
Próby: 0Ładowanie...

Zdefiniuj wycinek przypadków anonimowej struktury z polami in i want, wypełniony 3 przypadkami do przetestowania Abs (dodatni, ujemny, zero).

Ładowanie edytora...

Rozwiązanie dostępne po 3 próbach

Ćwiczenie#go.m9.l2.e2
Próby: 0Ładowanie...

Dodaj t.Run z tc.name, aby zamienić każdy przypadek w nazwany podtest.

Ładowanie edytora...

Rozwiązanie dostępne po 3 próbach

Quiz#go.m9.l2.e3
Gotowe

Jaka jest główna zaleta wzorca opartego na tabeli w porównaniu z N oddzielnymi funkcjami testowymi?

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