Go

Write an idiomatic table-driven test with subtests. Why is this the preferred Go style?

Question 378MediumGo 1.22 to 1.25
func TestParseDuration(t *testing.T) {
    tests := []struct {
        name    string
        in      string
        want    time.Duration
        wantErr bool
    }{
        {name: "seconds", in: "5s", want: 5 * time.Second},
        {name: "mixed", in: "1h30m", want: 90 * time.Minute},
        {name: "empty", in: "", wantErr: true},
        {name: "garbage", in: "abc", wantErr: true},
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got, err := time.ParseDuration(tt.in)
            if (err != nil) != tt.wantErr {
                t.Fatalf("ParseDuration(%q) err = %v, wantErr %v", tt.in, err, tt.wantErr)
            }
            if got != tt.want {
                t.Errorf("ParseDuration(%q) = %v, want %v", tt.in, got, tt.want)
            }
        })
    }
}

Benefits: adding a case is one line; each case is named and individually runnable (go test -run 'TestParseDuration/mixed'); t.Fatal in one subtest doesn't abort the others; failure messages follow the Go convention Func(input) = got, want want.

Gotchas: use named fields in the struct literals so the table stays readable; use a map keyed by name only if order doesn't matter (map iteration is randomized, which is actually a nice way to catch order-dependence); prefer go-cmp's cmp.Diff for comparing structs instead of reflect.DeepEqual so you get a readable diff. Don't put logic (if/else on the case) inside the loop body — split into separate tests instead. Since Go 1.22 each loop iteration has its own tt, so the old tt := tt copy is no longer needed even when subtests call t.Parallel().

More on Performance, Profiling & Testing

All 38 Performance, Profiling & Testing questions