How to Use t.Run for Subtests in Go

Use `t.Run(name, func(t *testing.T))` to define subtests within a parent test function, allowing you to run multiple test cases in parallel while keeping the test output organized by name.

Use t.Run(name, func(t *testing.T)) to define subtests within a parent test function, allowing you to run multiple test cases in parallel while keeping the test output organized by name. This approach improves test isolation and enables parallel execution of independent cases using t.Parallel() inside the subtest.

Here is a practical example demonstrating how to structure subtests with parallel execution and proper cleanup:

func TestCalculate(t *testing.T) {
	tests := []struct {
		name     string
		input    int
		expected int
	}{
		{"double 5", 5, 10},
		{"double 0", 0, 0},
		{"double -3", -3, -6},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel() // Enable parallel execution for this subtest

			result := tt.input * 2
			if result != tt.expected {
				t.Errorf("got %d, want %d", result, tt.expected)
			}
		})
	}
}

When you run this test with go test -v, the output will clearly separate each subtest result. If you add the -race flag, the Go race detector will correctly identify data races between parallel subtests, which is a common pitfall if you share mutable state without synchronization.

To run only a specific subtest, use the -run flag with the full test name path:

go test -run TestCalculate/double_5 -v

This is useful for debugging a specific failing case without running the entire suite.

Key things to remember:

  1. Isolation: Each subtest gets its own *testing.T instance. Do not share mutable global variables between subtests unless you synchronize access, as they may run in parallel.
  2. Failures: If a subtest fails, the parent test continues running other subtests unless you call t.FailNow() or t.Fatal() inside the subtest.
  3. Cleanup: Use t.Cleanup() inside the subtest function to ensure resources are released even if the subtest fails or is skipped.
t.Run("with cleanup", func(t *testing.T) {
    t.Parallel()
    file, err := os.CreateTemp("", "test")
    if err != nil {
        t.Fatal(err)
    }
    // Ensure file is removed even if test fails
    t.Cleanup(func() {
        os.Remove(file.Name())
    })
    // Test logic here
})

This pattern is the standard way to organize complex test scenarios in Go, making your test suite faster and easier to maintain.