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

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

References