Go's testing infrastructure is built into the language toolchain, not bolted on as a third-party framework. Files ending in _test.go are compiled only during go test. Functions named TestXxx(t *testing.T) are unit tests, BenchmarkXxx(b *testing.B) are benchmarks, and ExampleXxx() are documentation tests verified by comparing stdout to a comment. No test runner to install, no configuration files — go test ./... runs every test in the module.
The testing.T type provides Error, Fatal, Log, Skip, Run (for subtests), Helper (for cleaner stack traces), Parallel (for concurrent execution), and Cleanup (for teardown). Table-driven tests — a slice of test cases iterated with t.Run — are the dominant pattern in the Go ecosystem. They keep test logic DRY, produce named sub-test output, and allow running a single case with go test -run TestXxx/case_name.
The standard library's testing package is deliberately minimal: it provides failure reporting but not assertions. The community is split between if got != want { t.Errorf(...) } (pure stdlib, zero dependencies) and assertion libraries like testify/assert (less boilerplate, richer diff output). Both approaches work well. The right choice depends on team preference and whether you want to keep test dependencies at zero.
Master go testing
Take the Go Programming course with hands-on lessons and challenges.
Table-driven tests define cases as a struct slice and loop over them with t.Run. Each case is a named subtest, so failures identify exactly which case broke. You can run a single case with -run TestAdd/case_name. This pattern scales from trivial functions to complex integration scenarios.
t.Helper() marks functions as test helpers so error messages show the test's line number. t.Cleanup() registers teardown that runs after the test finishes, replacing manual defer in test functions. Generic assertEqual works for any comparable type without testify.
t.Parallel() marks subtests for concurrent execution. Combined with -race, this catches data races in handler code. httptest.NewRequest and httptest.NewRecorder let you test HTTP handlers without starting a real server. Each parallel subtest gets its own recorder, so there is no shared mutable state.
Benchmark functions iterate b.N times; the framework adjusts N until timing stabilizes. Sub-benchmarks with b.Run compare implementations across input sizes. The -benchmem flag adds allocation statistics. This benchmark demonstrates that strings.Builder is O(n) while naive concatenation is O(n^2).
Example functions serve triple duty: they appear in go doc as runnable examples, they execute as tests verifying the Output comment matches stdout, and they show up in pkg.go.dev documentation. The // Output: comment is compared exactly. Use // Unordered output: when order is nondeterministic.
testify/assert continues the test on failure (like t.Error), while testify/require stops immediately (like t.Fatal). assert.Contains, assert.Len, assert.ElementsMatch, and assert.InDelta provide richer assertions than raw if-statements. Whether to use testify or pure stdlib is a team decision.
Using the loop variable directly in parallel subtests instead of capturing it — all subtests see the same final value
In Go versions before 1.22, capture the loop variable with `tt := tt` at the start of the loop body before calling t.Run with t.Parallel(). Go 1.22+ fixed loop variable scoping, so each iteration gets a fresh variable. If your module targets Go 1.22+, the capture is unnecessary.
Writing benchmark code outside the b.N loop — setup that runs once gets amortized across iterations, skewing results
Only the code under measurement belongs inside `for range b.N { ... }`. Put setup code before the loop and use b.ResetTimer() if setup is expensive. Use b.StopTimer()/b.StartTimer() for per-iteration setup that shouldn't be measured.
Using t.Fatal or require inside a goroutine spawned by a test — this panics instead of failing the test cleanly
t.Fatal (and require.Xxx) call runtime.Goexit(), which only works on the test goroutine. In spawned goroutines, use t.Error (or assert.Xxx) and coordinate with sync.WaitGroup or channels. Alternatively, use t.Run subtests with t.Parallel() instead of raw goroutines.
Not running tests with -race flag in CI, missing data races that only manifest under concurrent execution
Always run `go test -race ./...` in your CI pipeline. The race detector instruments memory accesses at compile time and catches races at runtime. It has roughly 2-10x overhead, which is acceptable for CI. Many critical bugs are invisible without it.
Go testing is built into the toolchain: _test.go files, TestXxx functions, and `go test` are all you need. Table-driven tests with t.Run are the dominant pattern, providing named subtests, isolated failure reporting, and selective execution. t.Helper and t.Cleanup keep helpers clean. Benchmarks use b.N iteration, example tests double as verified documentation, and the -race flag catches concurrency bugs. The stdlib testing package is minimal by design; testify adds richer assertions for teams that prefer less boilerplate.
Interactive lessons and challenges on Stanza, practice in VS Code, Cursor, or the web.
Interactive lessons and challenges, right in your code editor.
Check the free courses. No credit card.