Go Testing
Go ships testing in the standard toolchain. There's no framework to choose: write functions named TestXxx(t *testing.T) in _test.go files and run go test ./.... The same package handles benchmarks, fuzzing, examples (which double as documentation), and coverage, and the -race flag finds data races while your tests run.
Go testing culture favors plain code over assertion DSLs: table-driven tests with ordinary if comparisons, fakes built on small interfaces, and real dependencies (an in-memory HTTP server, a real database in a container) where practical.
TL;DR
- Tests live in
xxx_test.gonext to the code; functions arefunc TestXxx(t *testing.T). - Table-driven tests plus
t.Runsubtests are the idiomatic structure. - Use
t.Errorf(continue) vst.Fatalf(stop this test);t.Helper()fixes line numbers in helpers;t.Cleanupregisters teardown. net/http/httptesttests handlers and clients without a real network.- Fuzzing (
FuzzXxx) and benchmarks (BenchmarkXxx,b.Loop()) are built in. - Always run CI with
-race, and use-coverto see what's untested.
Quick Example
A table-driven test with subtests:
Adding a case is one line, and failures name the exact case: --- FAIL: TestSlugify/unicode.
Core Concepts
The testing.T API
Package Naming: Internal vs External Tests
Tests in package slug can access unexported identifiers (white-box). Tests in package slug_test only see the exported API (black-box), which is a good way to test your package as consumers use it. Both can live in the same directory.
Test Helpers
For comparing structs, github.com/google/go-cmp/cmp.Diff(want, got) produces readable diffs. Many teams also use testify for assertions, but the standard library alone is fully sufficient.
Testing HTTP With httptest
Fakes via Interfaces
Because Go interfaces are satisfied implicitly, code that depends on a small interface can be tested with a hand-written fake struct. No mocking framework is needed. See mocking and stubbing.
Fuzzing, Benchmarks, and Examples
Fuzzing
go test -fuzz=FuzzSlugify mutates inputs looking for failures and saves any crashers to testdata/fuzz/ as permanent regression tests. Fuzz tests check properties (no panics, round-trips, invariants) rather than exact outputs.
Benchmarks
go test -bench=. -benchmem reports ns/op and allocations per op. Compare runs statistically with benchstat. See benchmarking.
Examples
func ExampleSlugify() with an // Output: comment is compiled, run, and verified by go test, and it appears in the package's documentation on pkg.go.dev.
Best Practices
Run the Race Detector in CI
go test -race ./... instruments memory accesses and fails tests on data races. It's slower, but it catches concurrency bugs that are otherwise nearly impossible to reproduce. See concurrency patterns.
Write Failure Messages That Diagnose Themselves
Include the input, what you got, and what you wanted: Slugify(%q) = %q, want %q. A reader should understand the failure without opening the test.
Use Real Dependencies Where It's Cheap
For database code, spinning up a real Postgres (testcontainers-go, or a shared container in CI) catches SQL bugs that fakes hide. Keep those tests behind a build tag or testing.Short() so the fast unit suite stays fast. See integration testing.
Use Golden Files for Large Outputs
For rendered templates, generated code, or large JSON, compare against files in testdata/, with an -update flag that rewrites them when output changes intentionally.
Common Mistakes
Loop Variable Capture (Before Go 1.22)
Go 1.22 made loop variables per-iteration, fixing this. On older versions, add tt := tt inside the loop.
Calling t.Fatal From a Goroutine
t.Fatal must be called from the goroutine running the test. From another goroutine it doesn't stop the test correctly. Send errors back over a channel, or use t.Error, which is safe from any goroutine.
Sleeping to Wait for Things
time.Sleep(100 time.Millisecond) makes tests slow and* flaky. Synchronize with channels or sync.WaitGroup, poll with a deadline, or use the testing/synctest package (Go 1.24+), which runs tests with a fake clock.
FAQ
Do I need an assertion library like testify?
No. The standard library plus go-cmp for deep comparisons covers most needs, and plain if got != want checks are idiomatic. testify is popular and fine if your team prefers it; just use it consistently.
How do I run only some tests?
go test -run 'Regex' matches test names, and subtests use / (for example -run 'TestSlugify/unicode'). -skip excludes by pattern, -short lets long tests skip themselves, and build tags (//go:build integration) separate suites.
How do I measure coverage?
go test -coverprofile=cover.out ./... then go tool cover -html=cover.out for a line-by-line view. Coverage shows what's unexecuted; it doesn't prove behavior is checked. See test coverage.
Why are my test results cached?
go test caches successful results for packages whose code and inputs haven't changed, which is why reruns print (cached). Use -count=1 to force a rerun, for example for tests that touch external systems.
Related Topics
- Go — The language overview
- Go Interfaces — Enabling fakes for testing
- Unit Testing — Principles across languages
- Benchmarking — Measuring performance reliably
- Test Coverage — What coverage does and doesn't tell you
- Concurrency Patterns — Code the race detector protects