पाठ 21 / 26
Table-Driven Tests
Many cases, one loop.
testing package and subtests
Tests live in _test.go files with functions TestXxx(t *testing.T). The idiomatic style is table-driven: a slice of cases (name, inputs, expected outputs) and a loop that runs each as a subtest with t.Run, so failures name the exact case. go test -v shows each subtest, -cover reports coverage, -run filters by name, -race adds race detection, and benchmarks (BenchmarkXxx) and fuzz tests (FuzzXxx) use the same tool.
Built-in quality tools
go test, go vet and gofmt are part of the toolchain and used by every Go project.
The code under test
A discount function with a sentinel error.
package main
import "errors"
var ErrBadCode = errors.New("unknown discount code")
func Discount(total float64, code string) (float64, error) {
switch code {
case "":
return total, nil
case "SAVE10":
return total * 0.9, nil
case "FLAT50":
if total >= 50 {
return total - 50, nil
}
return total, nil
}
return 0, ErrBadCode
}
func main() {}A table-driven test, run
I ran this with Go 1.27.1 (go test -count=1 -v -cover . 2>&1 | sed -E "s/\(([0-9.]+)s\)/(time)/; s/\t[0-9.]+s(\t|$)/\t(time)\1/" in a module named demo, standard library only). Five cases run as named subtests, including the boundary and the error path, all pass with 100% statement coverage. Timings were replaced with (time) to keep the output stable.
package main
import (
"errors"
"testing"
)
func TestDiscount(t *testing.T) {
tests := []struct {
name string
total float64
code string
want float64
wantErr error
}{
{"no code", 100, "", 100, nil},
{"percent", 100, "SAVE10", 90, nil},
{"flat at boundary", 50, "FLAT50", 0, nil},
{"flat below minimum", 40, "FLAT50", 40, nil},
{"unknown code", 100, "BOGUS", 0, ErrBadCode},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Discount(tt.total, tt.code)
if !errors.Is(err, tt.wantErr) {
t.Fatalf("err = %v, want %v", err, tt.wantErr)
}
if got != tt.want {
t.Errorf("Discount(%v, %q) = %v, want %v", tt.total, tt.code, got, tt.want)
}
})
}
}
Output:
=== RUN TestDiscount
=== RUN TestDiscount/no_code
=== RUN TestDiscount/percent
=== RUN TestDiscount/flat_at_boundary
=== RUN TestDiscount/flat_below_minimum
=== RUN TestDiscount/unknown_code
--- PASS: TestDiscount (time)
--- PASS: TestDiscount/no_code (time)
--- PASS: TestDiscount/percent (time)
--- PASS: TestDiscount/flat_at_boundary (time)
--- PASS: TestDiscount/flat_below_minimum (time)
--- PASS: TestDiscount/unknown_code (time)
PASS
coverage: 100.0% of statements
ok demo (time) coverage: 100.0% of statementsत्वरित जाँच: Why use t.Run for each table case?
- It makes tests run in production
- Each case becomes a named subtest, so failures point to the exact case
- It disables coverage
- It is required to compile
Answer
Each case becomes a named subtest, so failures point to the exact case — Named, isolated cases.