Subtests with t.Run in Go

Use t.Run to give each test case its own name and result, run a single subtest with -run, use Fatal safely inside cases, and run subtests in parallel.

t.Run starts a subtest: a named test inside another test. Combined with a table of cases, it gives each case its own line in the output, lets t.Fatalf stop just that case, and lets you run one case by name.

Turning Table Rows Into Subtests

// file: slug_test.go
package textutil

import (
	"strings"
	"testing"
)

func Slug(title string) string {
	return strings.Join(strings.Fields(strings.ToLower(title)), "-")
}

func TestSlug(t *testing.T) {
	tests := []struct {
		name, input, want string
	}{
		{"two words", "Hello World", "hello-world"},
		{"extra spaces", "  Go   Docs  ", "go-docs"},
		{"empty", "", ""},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			if got := Slug(tc.input); got != tc.want {
				t.Errorf("Slug(%q) = %q, want %q", tc.input, got, tc.want)
			}
		})
	}
}

With go test -v the output shows each case:

=== RUN   TestSlug
=== RUN   TestSlug/two_words
=== RUN   TestSlug/extra_spaces
=== RUN   TestSlug/empty
--- PASS: TestSlug (0.00s)
    --- PASS: TestSlug/two_words (0.00s)
    --- PASS: TestSlug/extra_spaces (0.00s)
    --- PASS: TestSlug/empty (0.00s)

Spaces in names become underscores. The inner function receives its own t, which belongs to the subtest. Always use that inner t, not the outer one.

Running One Subtest

The full name of a subtest is Parent/child, so -run can select it:

go test -run 'TestSlug/extra_spaces' -v

This is especially handy when one case in a long table fails and you want to debug it alone.

Fatal Inside a Subtest

Inside t.Run, t.Fatalf stops only the current subtest. The loop continues with the next case:

// file: parse_test.go
package parse

import (
	"strconv"
	"testing"
)

func TestAtoi(t *testing.T) {
	for _, in := range []string{"1", "22", "333"} {
		t.Run(in, func(t *testing.T) {
			n, err := strconv.Atoi(in)
			if err != nil {
				t.Fatalf("Atoi(%q): %v", in, err)
			}
			if len(strconv.Itoa(n)) != len(in) {
				t.Errorf("round trip changed length for %q", in)
			}
		})
	}
}

Without subtests, a Fatalf on the first bad row would skip all the remaining rows.

Parallel Subtests

Call t.Parallel() inside the subtest to run cases at the same time:

// file: parallel_test.go
package slow

import (
	"testing"
	"time"
)

func TestSlowCases(t *testing.T) {
	for _, d := range []time.Duration{30, 30, 30} {
		t.Run(d.String(), func(t *testing.T) {
			t.Parallel()
			time.Sleep(d * time.Millisecond)
		})
	}
}

The three subtests together take about 30ms instead of 90ms.

Loop variables before Go 1.22:

Since Go 1.22, each iteration has its own tc, so parallel subtests see the right case. On modules with go 1.21 or older in go.mod, all parallel subtests saw the last case. The old fix was tc := tc at the top of the loop body, which you may still see in older code.

Setup and Teardown Around Subtests

Code before the loop runs once for all subtests, which is a natural place for shared setup:

func TestStore(t *testing.T) {
	db := openTestDB(t) // shared setup
	t.Run("insert", func(t *testing.T) { ... })
	t.Run("delete", func(t *testing.T) { ... })
}

Note that if subtests call t.Parallel(), the parent function returns before they finish. Use t.Cleanup for teardown, which waits for parallel subtests.

Using the outer t inside t.Run:

Calling the outer test's t.Fatalf inside the subtest function stops the whole parent test from the wrong goroutine and reports the failure under the wrong name. Name the parameter t so it shadows the outer one, and the mistake cannot happen.

Clean subtest output:

If go test -v lists every case by a readable name, and you can rerun any failing case with -run 'TestX/case_name', your subtests are set up well.

Summary

t.Run(name, func(t *testing.T) {...}) creates a named subtest with its own result. Use it for each row of a table so failures are labeled, Fatalf stops only that case, and -run 'Parent/child' reruns one case. Add t.Parallel() for independent cases, use the inner t, and put teardown in t.Cleanup.