One Test, Many Cases
Most functions need the same check for many inputs: normal values, negative values, zero, edge cases. Copying the test five times makes five places to maintain.
The Go way is a table-driven test: put the cases into a slice of structs — each case holds a name, the inputs and the expected output — and run one loop over them.
Adding a new case becomes one new line in the table. This is the most common test pattern you will see in real Go code.
The Table
The table is an anonymous struct slice, defined right inside the test. Each entry is one case: a name (for the failure message), the inputs, and the expected result.
The loop calls the function once per case and reports each mismatch with t.Errorf — so every broken case shows up, not just the first.
Four cases in one table
Four cases, one loop, one check. All pass here. To test a fifth situation you would add one line to the table — the loop stays untouched.
Output: Go test file and captured go test output
package calc
import "testing"
func Add(a, b int) int {
return a + b
}
func TestAdd(t *testing.T) {
cases := []struct {
name string
a, b int
want int
}{
{"one plus one", 1, 1, 2},
{"positive and negative", 5, -3, 2},
{"both negative", -2, -4, -6},
{"zeros", 0, 0, 0},
}
for _, tc := range cases {
got := Add(tc.a, tc.b)
if got != tc.want {
t.Errorf("%s: Add(%d, %d) = %d; want %d", tc.name, tc.a, tc.b, got, tc.want)
}
}
}
Subtests with t.Run
t.Run(name, func(t *testing.T) { … }) turns each table case into its own subtest. The case names now appear in the go test -v output, like TestAdd/both_negative (spaces become underscores).
Each subtest passes or fails on its own, which makes a broken case easy to spot in a long table.
Every case gets a name
The same table, wrapped in t.Run. The verbose output now lists all four cases separately — you can see exactly which cases ran and passed.
Output: Go test file and captured go test output
package calc
import "testing"
func Add(a, b int) int {
return a + b
}
func TestAdd(t *testing.T) {
cases := []struct {
name string
a, b int
want int
}{
{"one plus one", 1, 1, 2},
{"positive and negative", 5, -3, 2},
{"both negative", -2, -4, -6},
{"zeros", 0, 0, 0},
}
for _, tc := range cases {
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)
}
})
}
}
Run One Case Alone
Subtests can be selected from the command line with -run, using TestName/subtest_name:
go test -v -run "TestAdd/both_negative"
Only the matching subtest runs. This is perfect while fixing one broken case: rerun just that case until it is green, then run everything again.
Filter with -run
The same file as above, run with -run "TestAdd/both_negative". Only one of the four cases executes — the others are not even started.
Output: Go test file and captured go test output
package calc
import "testing"
func Add(a, b int) int {
return a + b
}
func TestAdd(t *testing.T) {
cases := []struct {
name string
a, b int
want int
}{
{"one plus one", 1, 1, 2},
{"positive and negative", 5, -3, 2},
{"both negative", -2, -4, -6},
{"zeros", 0, 0, 0},
}
for _, tc := range cases {
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)
}
})
}
}