Go Testing Culture: Simple, Fast, Table-Driven
Go treats tests as ordinary code in the same module, compiled by the same toolchain, and run with one command.
Search across all documentation pages
Go treats tests as ordinary code in the same module, compiled by the same toolchain, and run with one command.
That design nudges teams toward small, fast, readable tests instead of heavyweight harnesses.
testing package plus go test is the default quality layer - no JUnit-style XML forests, no mandatory assertion libraries, and no separate test runner process to configure.TestXxx, BenchmarkXxx, ExampleXxx, FuzzXxx, table-driven tests, t.Run subtests, -race, coverage profiles.Go ships a single test vocabulary in the standard library.
You name functions TestSomething(t *testing.T), place them in something_test.go next to something.go, and run go test.
The compiler builds a test binary that links your package plus the test files; go test executes it and reports pass/fail.
There is no global setup file required for most packages.
Simple means readers see the arrange-act-assert flow in plain Go.
A failed assertion is usually if got != want { t.Fatalf(...) } or a thin helper - not a DSL with twenty matchers.
Fast is a cultural expectation backed by tooling.
go test caches successful package results keyed by inputs; unchanged packages skip re-execution.
Teams target sub-second package tests so go test ./... runs on every commit.
Table-driven is the idiomatic way to add cases without copy-pasting test functions.
One slice of structs holds name, inputs, and expected outputs; a loop calls t.Run per row.
The table is the spec; the loop is the harness.
production.go *_test.go
| |
+-------- same package -+
|
go test ./...
|
pass / fail per package
Package modes: go test can run tests in the package under test (package foo) or as an external test package (package foo_test) to exercise only exported APIs.
Both compile with the same command; the choice signals intent to consumers.
Subtests (t.Run) give hierarchical names in output and allow parallel execution with t.Parallel() inside a table loop.
Failures identify the case name (TestParse/empty_input) instead of a line number in a monolithic test.
Examples are tests that also appear in documentation.
An Example function's // Output: comment is verified by go test, keeping godoc honest.
Benchmarks measure ns/op and allocations; fuzz tests mutate inputs to find crashes.
All three use the same file naming and go test entry point with different function signatures.
Race detector (go test -race) instruments memory access to catch data races that pass under normal scheduling.
It is slower and belongs in CI or pre-push, not every save.
func TestAdd(t *testing.T) {
tests := []struct{ a, b, want int }{{1, 2, 3}}
for _, tc := range tests {
t.Run("", func(t *testing.T) {
if got := Add(tc.a, tc.b); got != tc.want {
t.Fatalf("got %d want %d", got, tc.want)
}
})
}
}Third-party libraries like testify add assertions and mocks; httptest from net/http/httptest is stdlib for handler tests.
Teams often adopt testify for ergonomics but keep table-driven structure and go test as the runner.
| Practice | Strength | Weakness | Best fit |
|---|---|---|---|
| Pure stdlib tests | Zero deps, always compiles | Verbose comparisons | Libraries, OSS |
| testify assertions | Readable failures | Extra module | Large app teams |
| Heavy integration DB tests | Catches real SQL bugs | Slow CI, flaky without containers | Few smoke paths |
| Fuzz + race in CI | Finds crashes and races | CPU and time cost | Security-sensitive parsers |
Test pyramid for Go services: many fast tests on pure functions and handlers with httptest; fewer tests hitting real databases with testcontainers; end-to-end outside go test or in a nightly job.
Monorepos: go test ./... at the module or workspace root respects go.work; cache hits scale with unchanged packages.
Generics and table-driven: type parameters do not change the pattern - tables can hold typed inputs; subtests still name cases.
Coverage gates: go test -coverprofile feeds go tool cover and CI thresholds; high percentage without meaningful assertions is a known anti-pattern.
pprof explains where time goes in production.Co-location keeps examples accurate when APIs change.
go test compiles both together, so tests break at compile time when signatures move.
Package-level unit tests should usually finish in milliseconds to low seconds.
If go test ./... exceeds a few minutes, split integration suites or use build tags.
No, but it is idiomatic for functions with multiple input/output pairs.
Single-path tests can stay a flat function without a table.
Successful runs cache results per package hash.
Use go test -count=1 to disable cache when debugging flaky tests.
package foo_test imports foo as a consumer would - good for API contracts.
package foo can test unexported helpers.
Prefer small interfaces in production code and hand-written fakes in _test.go.
Heavy mock generators are optional, not default.
Examples run under go test and render in pkg.go.dev when they include // Output: comments.
They document public API usage, not exhaustive edge cases.
On dedicated jobs or nightly, not every PR, unless comparing against a stored baseline.
Benchmarks are noisy on shared runners.
No. Fuzzing explores unexpected inputs; tables document expected behavior reviewers can read.
Linters catch static issues; tests catch behavior.
CI should run both - see linting-quality gates for ordering.
Sleep makes tests slow and flaky under load.
Use channels, sync.WaitGroup, or httptest response control instead.
Usually no - tests belong in the same module as the code they exercise.
Separate e2e modules are for black-box deployment tests.
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).
Reviewed by Chris St. John·Last updated Jul 16, 2026