How do subtests work with t.Run, and why are they useful?
Quick Answer
t.Run(name, func(t *testing.T) { ... }) runs a named subtest as its own logical test, with its own pass/fail status reported separately in go test -v output, and its own ability to call t.Fatal/t.Skip without stopping sibling subtests in the same table. This gives you granular failure reporting ("TestAdd/negative_numbers failed", not just "TestAdd failed" with no indication which case broke), the ability to run a single named case in isolation with go test -run TestAdd/negative_numbers, and, when combined with t.Parallel() inside the subtest closure, the ability to run independent subtests concurrently for faster test suites.
Detailed Answer
t.Run turns a flat test function into a tree of independently reportable, independently runnable subtests.
Granular failure reporting
func TestValidate(t *testing.T) {
cases := map[string]struct {
input string
valid bool
}{
"empty": {"", false},
"valid": {"ok@example.com", true},
"no-at": {"nope", false},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
got := Validate(tc.input)
if got != tc.valid {
t.Errorf("Validate(%q) = %v, want %v", tc.input, got, tc.valid)
}
})
}
}
go test -v -run TestValidate
=== RUN TestValidate/empty
--- PASS: TestValidate/empty
=== RUN TestValidate/no-at
--- FAIL: TestValidate/no-at
=== RUN TestValidate/valid
--- PASS: TestValidate/valid
You immediately know no-at is the failing case, without scanning error messages to figure out which table entry broke.
Running a single subtest
go test -run TestValidate/no-at
The / in -run targets a specific named subtest, which is invaluable when iterating on a fix for one failing case without re-running the entire suite every time.
Parallel subtests
for name, tc := range cases {
tc := tc // capture for the closure (needed pre-Go 1.22)
t.Run(name, func(t *testing.T) {
t.Parallel() // this subtest runs concurrently with other parallel subtests
// ...
})
}
Calling t.Parallel() inside a subtest tells the test runner it can run concurrently with sibling subtests that also called t.Parallel(), which can meaningfully speed up test suites with many independent, non-interfering cases.
Why this matters practically
Subtests are what make table-driven tests genuinely useful at scale. Without t.Run, a single failing case in a 50-entry table just reports "TestFoo failed," and you're left adding print statements to figure out which one.