Table-driven tests
Write concise, comprehensive tests with the table-driven pattern — subtests, t.Run, t.Parallel, and benchmarks.
- Write a table-driven test using a slice of struct test cases
- Run subtests with t.Run to get individual named results
- Mark subtests safe to run in parallel with t.Parallel
- Write a BenchmarkXxx function and interpret go test -bench output
- Explain what b.N is in a benchmark loop
The most common complaint about test code is duplication: you write the same assertion logic ten times with ten different inputs. Go's answer is the table-driven test — a single test function driven by a slice of cases. It is so prevalent in the Go standard library and ecosystem that it is considered idiomatic. Once you adopt it, you will rarely write repetitive tests again.
The table-driven pattern
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
want int
}{
{"positive", 2, 3, 5},
{"zero", 0, 0, 0},
{"negative", -1, -2, -3},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got := Add(tc.a, tc.b)
if got != tc.want {
t.Errorf("Add(%d, %d) = %d; want %d", tc.a, tc.b, got, tc.want)
}
})
}
}Key points:
- The table is an anonymous struct slice. Fields match what the test needs.
t.Run(name, func)creates a subtest — a named, individually reportable unit.- Adding a new case is one line in the table, not a new test function.
Reading subtest output
With -v, each subtest prints its name:
--- PASS: TestAdd (0.00s)
--- PASS: TestAdd/positive (0.00s)
--- PASS: TestAdd/zero (0.00s)
--- PASS: TestAdd/negative (0.00s)You can run a single subtest by name:
go test -run TestAdd/positive ./...The / separator works as a matcher between the test name and the subtest name. This is invaluable when debugging a specific failing case.
t.Parallel — running subtests concurrently
Mark a subtest as safe to run in parallel with other parallel subtests:
for _, tc := range tests {
tc := tc // capture range variable — essential!
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// ... test body ...
})
}The tc := tc line captures the loop variable inside the closure. Without it, all subtests share the same tc reference and will likely operate on the last element in the table by the time they run. This is one of Go's most common concurrency bugs in test code. (In Go 1.22+ the loop variable semantics changed, but capturing explicitly is still the safe habit.)
Parallel subtests speed up long test suites significantly when the test cases are independent. Don't use t.Parallel() when subtests share mutable state.
Benchmarks
A benchmark function starts with Benchmark and takes *testing.B:
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(100, 200)
}
}b.N is chosen automatically by the testing framework: it increases the number until the benchmark runs long enough for a stable measurement.
Run benchmarks explicitly (they are skipped by default):
go test -bench=. ./...
go test -bench=BenchmarkAdd -benchmem ./...Typical output:
BenchmarkAdd-8 1000000000 0.308 ns/op1000000000— how many iterations ran.0.308 ns/op— average time per iteration.-benchmemaddsB/op(bytes allocated per operation) andallocs/op.
Benchmarks measure relative performance. The absolute numbers vary by machine. What matters is the ratio before and after an optimisation, run on the same hardware. Always run benchmarks multiple times and check for variance.
Resetting the timer
If your benchmark has setup that should not count toward the time:
func BenchmarkProcess(b *testing.B) {
data := generateLargeDataset() // setup
b.ResetTimer() // start timing from here
for i := 0; i < b.N; i++ {
Process(data)
}
}b.ResetTimer() discards the elapsed time up to that point so setup does not skew results.
Check your understanding
Knowledge check
- 1.In a range loop over test cases before Go 1.22, why do you write tc := tc inside the loop before calling t.Run?
- 2.In a benchmark loop (for i := 0; i < b.N; i++), what determines the value of b.N?
- 3.Benchmark functions run automatically when you run go test ./...
Do it yourself
Write a table-driven test for a Contains(s, substr string) bool function with at least four cases (empty string, match at start, match at end, no match). Run with -v to see the subtest names, then add one more case:
go test -v -run TestContains ./...Where to go next
The lab consolidates package visibility, module versioning, table test structure, and benchmark interpretation through scenario questions.