Go

Go Testing👨‍💻

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.

Key Takeaways

  • 1Test files end in `_test.go` and are excluded from production builds — test functions must be `TestXxx(t *testing.T)` with an uppercase letter after Test
  • 2Table-driven tests with `t.Run` are the standard pattern: a slice of named cases, each executed as a subtest with isolated failure reporting and the ability to run individually
  • 3`t.Helper()` marks a function as a test helper so that failure messages report the caller's line number, not the helper's — essential for reusable assertion functions
  • 4`t.Parallel()` marks a test or subtest for concurrent execution, which finds race conditions and speeds up I/O-bound test suites
  • 5Benchmark functions use `b.N` as the iteration count — the framework increases N until timing is statistically stable, so the loop body must be the code under test
  • 6Example functions are both documentation and tests: they appear in `go doc` output and fail if their `// Output:` comment doesn't match actual stdout

Master go testing

Take the Go Programming course with hands-on lessons and challenges.

Examples

Table-driven tests with subtests

go

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.

Test helpers with t.Helper and t.Cleanup

go

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.

Parallel subtests for concurrent testing

go

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.

Benchmarks with b.N and sub-benchmarks

go

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 tests for documentation and verification

go

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.

Testing with testify assertions and mocks

go

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.

Common Mistakes

Mistake:

Using the loop variable directly in parallel subtests instead of capturing it — all subtests see the same final value

Fix:

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.

Mistake:

Writing benchmark code outside the b.N loop — setup that runs once gets amortized across iterations, skewing results

Fix:

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.

Mistake:

Using t.Fatal or require inside a goroutine spawned by a test — this panics instead of failing the test cleanly

Fix:

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.

Mistake:

Not running tests with -race flag in CI, missing data races that only manifest under concurrent execution

Fix:

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.

Best Practices

  • Use table-driven tests as the default pattern — they're concise, add new cases trivially, produce named output, and support running individual cases with -run
  • Mark all test helpers with t.Helper() so failure stack traces point to the test function, not the helper — this makes failures immediately actionable
  • Run `go test -race -cover ./...` in CI to catch data races and track coverage — aim for meaningful coverage of business logic, not 100% line coverage
  • Use t.Cleanup() instead of defer in test functions — it runs after subtests complete and works correctly with t.Parallel(), unlike defer which runs when the parent function returns
  • Keep test packages separate (`package foo_test`) for black-box testing of the public API, and use `package foo` only when testing unexported internals

Summary

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.

Practice Go with hands-on challenges

Learn go testing hands-on in your IDE

Interactive lessons and challenges on Stanza, practice in VS Code, Cursor, or the web.

Related Concepts

Related Cheatsheets

Master Go with Stanza

Interactive lessons and challenges, right in your code editor.

Check the free courses. No credit card.