Testing and table-driven tests

Go · Practical · 14 min

Go's built-in testing is used by every project; table tests are the house style.

Tests live next to the code in files ending _test.go. A test is a function func TestXxx(t *testing.T) in the same package (or in pkg_test for black-box tests). Run them with go test ./...; add -v for verbose output, -run 'Parse/negative' to filter, -cover for coverage, and -count=1 to bypass the result cache.

*testing.T reports failures:

  • t.Errorf(...) records a failure and keeps going — preferred, so one run shows all problems.
  • t.Fatalf(...) stops this test immediately — use it when continuing makes no sense.
  • t.Helper() in helper functions makes failures point to the caller's line.
  • t.Cleanup(fn), t.TempDir() and t.Setenv() handle setup and teardown.

The idiomatic shape is a table-driven test: a slice of cases, each with a name, inputs and expected output, looped over with t.Run(tc.name, ...). Each case becomes a subtest that can be run alone and reports separately. Adding a case is one line, which encourages covering edge cases.

Go's style is plain comparisons and clear messages — got X, want Y — rather than assertion libraries. For deep comparisons use reflect.DeepEqual, slices.Equal, or a diff library such as go-cmp.

Calling t.Parallel() lets subtests run concurrently. Example functions (func ExampleAdd() with an // Output: comment) are compiled, run, and shown in the docs, and go test -fuzz adds native fuzzing.

Example

// abs_test.go
package mathx

import "testing"

func Abs(n int) int {
	if n < 0 {
		return -n
	}
	return n
}

func TestAbs(t *testing.T) {
	tests := []struct {
		name string
		in   int
		want int
	}{
		{"positive", 3, 3},
		{"negative", -4, 4},
		{"zero", 0, 0},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			if got := Abs(tc.in); got != tc.want {
				t.Errorf("Abs(%d) = %d, want %d", tc.in, got, tc.want)
			}
		})
	}
}

Remember

  • Tests are TestXxx(t *testing.T) in _test.go files; run go test ./...
  • Table-driven tests + t.Run subtests are the idiomatic structure
  • Prefer t.Errorf to keep going; t.Fatalf when you can't continue

Watch out for

  • Using t.Fatal from a goroutine other than the test's own — it must be called from the test goroutine.

Check yourself

  1. How do you run only the "negative" case of TestAbs?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: pkg.go.dev/testing