Go Testing Package Tutorial
Write effective tests in Go with the standard testing package: table-driven tests, subtests, fixtures, parallelism, benchmarks, and fuzzing.
What you'll learn
- ✓Writing and running tests with go test
- ✓Structuring tests as table-driven cases
- ✓Using subtests and t.Run for clarity
- ✓Benchmarks and how to interpret allocations
- ✓Property-style testing with Go fuzzing
Prerequisites
- •Comfort with Go syntax and packages
What and Why
Go ships with a testing framework in the standard library: the testing package and the go test command. There is no separate runner, no XML config, no fixtures DSL. You write functions named TestXxx(t *testing.T), run go test ./..., and you are done.
This minimalism is the point. With one obvious way to write tests, every Go codebase tests the same way, and tooling (coverage, race detection, fuzzing, benchmarks) all live in the same command.
Mental Model
Tests are normal Go code in _test.go files alongside the code they exercise.
func TestXxx(t *testing.T) unit test
func BenchmarkXxx(b *testing.B) benchmark; -bench flag
func FuzzXxx(f *testing.F) fuzz target; go test -fuzz
func TestMain(m *testing.M) custom setup/teardown
ExampleXxx() / Example(t) doc + check
t.Run("name", func(t *testing.T)) subtests Hands-on Example
A common pattern: table-driven tests with subtests.
package strings_test
import (
"strings"
"testing"
)
func TestTrimSpace(t *testing.T) {
cases := []struct {
name string
in string
want string
}{
{"empty", "", ""},
{"no spaces", "abc", "abc"},
{"leading", " abc", "abc"},
{"trailing", "abc ", "abc"},
{"both", " abc ", "abc"},
{"only spaces", " ", ""},
}
for _, tc := range cases {
tc := tc // capture range var for parallel
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
got := strings.TrimSpace(tc.in)
if got != tc.want {
t.Errorf("TrimSpace(%q) = %q; want %q", tc.in, got, tc.want)
}
})
}
}
Notes worth absorbing. t.Run creates a subtest with its own name, so failures point at the specific case. t.Parallel() runs subtests concurrently within the parent test. Capturing the loop variable into tc was historically required; in Go 1.22+ the loop variable is per-iteration, but the explicit form still reads clearly.
A benchmark looks similar but uses b.N:
func BenchmarkTrimSpace(b *testing.B) {
s := " hello world "
for i := 0; i < b.N; i++ {
_ = strings.TrimSpace(s)
}
}
Run with go test -bench=. -benchmem to see allocations per op.
A fuzz target generates inputs automatically:
func FuzzReverse(f *testing.F) {
f.Add("hello")
f.Fuzz(func(t *testing.T, s string) {
r := reverse(s)
if reverse(r) != s {
t.Errorf("reverse not involutive for %q", s)
}
})
}
go test -fuzz=FuzzReverse runs the fuzzer, mutating seed inputs to find failing cases.
Common Pitfalls
- Shared state across tests: package-level variables modified by tests cause order-dependent failures. Use
t.Cleanupto undo per-test mutations. - Hidden flakiness: tests that hit the network, the clock, or random sources fail intermittently. Inject dependencies (a fake clock, a deterministic RNG) instead.
t.Fatalfrom a goroutine: only callt.Fatalfrom the test goroutine. From other goroutines, send the error back on a channel and fail in the main goroutine.- Skipping when you meant to fail:
t.Skipremoves the test from the run. Use it for environment-dependent tests, not as a workaround for known-broken cases. - Benchmark setup inside the loop: any setup work between
for i := 0and the action under test is included in the timing. Move it out, or useb.ResetTimer().
Practical Tips
Run with -race regularly. Go’s race detector catches data races at runtime and is one of the most valuable features in the toolchain. CI should run tests with go test -race ./....
Use coverage incrementally: go test -cover ./... for a quick number, go test -coverprofile=cov.out for HTML with go tool cover -html=cov.out. Aim for coverage in the layers that change frequently rather than chasing a number.
For integration tests that need external resources, gate them with build tags: //go:build integration and run with go test -tags=integration ./....
The testing/quick package is older but still useful for quick property tests when fuzzing is overkill. The testify library is popular for assertions; the standard library is enough for most needs.
For test helpers, call t.Helper() at the top. Failures then point at the calling test, not the helper.
Wrap-up
Go testing rewards convention over configuration. Write _test.go files next to your code, use table-driven tests with subtests for readability, and add -race to your CI. When you need benchmarks or fuzzing, the same go test command takes you there with a flag. With these defaults, your tests stay close to the code they cover, run fast, and surface bugs the moment you change something.
Related articles
- Go Go Tooling: go vet, gofmt, and go test
A practical guide to Go's built-in tooling trio: gofmt for formatting, go vet for static checks, and go test for unit, benchmark, and coverage workflows.
- Go Go Testing Basics with the testing Package
Get productive with Go tests fast: writing _test.go files, table-driven tests with t.Run, running with go test flags, coverage, and an intro to benchmarks.
- Testing Property-Based Testing: An Introduction
Stop writing one example per test. Property-based testing generates inputs for you and finds the edge cases you would never think to write.
- Go Go Build Tags Explained
Use Go build tags to include or exclude files per OS, architecture, or custom condition. Learn the new //go:build syntax, common patterns, and how tags interact with the test runner.