Lektionen des Moduls (2/5)
Datengesteuerte Tests (Table-driven)
Tabellengesteuerte Tests sind das idiomatische Muster von Go: Anstatt N separate TestThisSpecificCase-Funktionen zu schreiben, deklarieren Sie einen Teil der Fälle und iterieren. Es gibt EINEN Assertionsblock und die Testdaten sind von der Logik getrennt.
Das Grundmuster
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)
}
})
}
}Drei Zutaten:
- Anonymer Strukturausschnitt mit den üblichen Feldern:
name, die Eingaben, die erwartete Ausgabe (und möglicherweise ein erwarteter Fehler). for _, tc := range cases– die Ausführungsschleife.t.Run(tc.name, func(t *testing.T) { ... })– jeder Fall wird zu einem Untertest mit seinem eigenen Namen.
Warum t.Run?
t.Run erstellt einen Untertest mit:
- Individueller Name wird in der Ausgabe angezeigt:
TestAbs/pos,TestAbs/neg, ... - Sein eigener
t *testing.T: Ein Fehler in einem Fall stoppt die anderen nicht (es sei denn,t.Fatalwird verwendet). - Befehlszeilenfilterung:
go test -run TestAbs/negführt nur in diesem Fall aus. - Optionale Parallelität: Rufen Sie
t.Parallel()innerhalb der Funktion auf, um zu parallelisieren.
go test -v -run TestAbs/negFälle mit erwarteten Fehlern
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)
}
})
}Die Loop-Closure-Falle (vor Go 1.22)
In Versionen vor Go 1.22 wurde die Variable tc über alle Iterationen hinweg gemeinsam genutzt: Der Aufruf von t.Parallel() innerhalb von t.Run führte dazu, dass alle Untertests für denselben letzten Fall ausgeführt wurden. Die Lösung war:
for _, tc := range cases {
tc := tc // local rebind (pre-1.22)
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// ...
})
}Benennung der Fälle
Best Practices:
- Ein kurzer, aber eindrucksvoller Name:
"empty input","too large","negative". - Keine Leerzeichen, wenn Sie einfach mit
-runfiltern möchten (oder Unterstriche verwenden). - Schließen Sie immer einen „Happy Path“-Fall, einen „Edge“-Fall (Null, leer) und mindestens einen Fehlerfall ein.
Übungen
Definieren Sie ein Cases-Slice einer anonymen Struktur mit den Feldern in und want, gefüllt mit 3 Cases zum Testen von Abs (positiv, negativ, null).
Lösung nach 3 Versuchen verfügbar
Fügen Sie t.Run mit tc.name hinzu, um jeden Fall in einen benannten Untertest umzuwandeln.
Lösung nach 3 Versuchen verfügbar
Was ist der Hauptvorteil des tabellengesteuerten Musters gegenüber N separaten Testfunktionen?
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) { ... })
}