The Go Programming Language — Testing#

Sampling note (XL tier): 1796 test files across the full src/ tree were surveyed via grep. Deep reads were focused on src/strings/strings_test.go, src/runtime/runtime_test.go, src/cmd/go/script_test.go, src/cmd/compile/script_test.go, src/internal/testenv/testenv.go, src/testing/testing.go, and a sample of testdata/script/*.txt files. Counts are derived from whole-tree grep commands.


Test metrics#

  • Test files: 1,796 *_test.go files in src/ (excl. vendor)
  • Source files: 5,102 non-test .go files in src/
  • Ratio (test / source): ~1:2.8 — roughly one test file per three source files
  • Test frameworks: Stdlib testing package only — zero third-party frameworks (no testify, gomock, ginkgo, gomega, goconvey)
  • Benchmark functions: 1,944 (func Bench*)
  • Example functions: 1,003 (func Example*)
  • Fuzz corpus targets: 292 (func Fuzz* / f.Fuzz(...))
  • t.Errorf / t.Fatal calls: 31,077 — entire assertion surface is stdlib

Test organization#

Placement#

Both internal (package foo) and external (package foo_test) test packages are used, frequently in the same directory. The dominant pattern across stdlib is package foo_test (external), which enforces that tests only exercise the public API. A dedicated bridge file — export_test.go — is used when internal state must be accessed.

The export_test.go bridge pattern#

44 files named export_test.go exist across the codebase. These files belong to the production package (e.g., package runtime) but are only compiled during go test, making private identifiers available to the external _test package via aliased vars:

// src/runtime/export_test.go
package runtime
var Fadd64 = fadd64  // exposes internal soft-float fn to runtime_test

This is the canonical Go solution to the black-box / white-box tension: by default, test the public API; when you must, create a curated export file rather than weakening package boundaries permanently.

Helper packages#

  • internal/testenv — the central cross-platform capability guard library. Provides MustHaveGoBuild(t), MustHaveCGO(t), SkipIfShortAndSlow(t), SkipFlaky(t, issue), CPUIsSlow(), etc. Used in 847 test files. Prevents tests from running in environments that cannot satisfy their requirements (no network, no C compiler, WASM runners). Replaces ad-hoc t.Skip() strings with well-typed, discoverable conditions.
  • internal/txtar — text archive format. A single .txt file can embed multiple named sub-files using -- filename -- separators. Used by the script test engine to ship multi-file test fixtures in one readable file.
  • cmd/internal/script and cmd/internal/script/scripttest — the internal script test engine powering cmd/go, cmd/compile, and cmd/link integration tests (described in detail below).
  • testing/fstest — provides MapFS (an in-memory fs.FS) and TestFS (a conformance checker for fs.FS implementations). Any package implementing fs.FS can test conformance with a single call.
  • testing/iotest — provides error-injecting wrappers (OneByteReader, HalfReader, ErrReader) for testing resilience of io.Reader / io.Writer consumers.
  • testing/quick — property-based testing via random value generation (frozen; use fuzz testing instead).
  • testing/slogtest — conformance testing for slog.Handler implementations.
  • testing/synctest (Go 1.24) — deterministic goroutine and timer testing. synctest.Run creates a “bubble” where goroutines and timers are controlled by a fake clock, enabling race-free tests of concurrent code without time.Sleep.

Fixtures#

  • 110 testdata/ directories across the codebase. Contents range from binary blobs (archive test cases), .go source fragments for the compiler, .txt script tests, and golden output files.
  • Golden file pattern: A -update flag is registered in tests that produce byte-for-byte deterministic output (e.g., compress/flate, compress/zlib). Running go test -update rewrites the golden files in testdata/ rather than failing. Discovered in: compress/flate/huffman_bit_writer_test.go:17.
  • Embedded corpus: Fuzz test corpus lives in testdata/fuzz/<FuzzFuncName>/ per the standard Go fuzz specification. Found in archive/tar, archive/zip, image/jpeg, image/png, internal/zstd.

Test patterns#

Table-driven tests#

  • Prevalence: Heavy — 2,706 occurrences of t.Run, testCases, tests :=, or tc.name patterns across *_test.go files
  • Style: Named struct slices with explicit field tags. The canonical form:
// src/strings/strings_test.go
type IndexTest struct {
    s   string
    sep string
    out int
}
var indexTests = []IndexTest{
    {"", "", 0},
    {"", "a", -1},
    ...
}
func TestIndex(t *testing.T) {
    for _, test := range indexTests {
        if actual := Index(test.s, test.sep); actual != test.out {
            t.Errorf("Index(%q, %q) = %v; want %v", test.s, test.sep, actual, test.out)
        }
    }
}
  • Sub-tests via t.Run: 797 uses of t.Parallel() combined with t.Run enable fine-grained parallelism at the sub-test level. This is the modern (Go 1.7+) form of table-driven testing.
  • Example: src/strings/strings_test.go contains dozens of named test tables (indexTests, linesTests, splitTests, etc.) — a textbook reference for the pattern.

Mocking approach#

  • Strategy: No mocking framework. The Go project relies on two strategies:
    1. Real implementations in tests — the stdlib tests exercise actual OS, filesystem, and network interfaces rather than mocks. testenv.MustHaveExternalNetwork(t) guards tests that need a real network.
    2. Interface fakes via testing/fstest.MapFS — for fs.FS-dependent code, an in-memory map is the fake. Similarly, bytes.Buffer acts as an io.Writer mock everywhere.
  • No generated mocks, no gomock, no mockery. The small-interface philosophy (io.Reader = one method) makes hand-written fakes trivial.

Fuzz testing#

  • 292 fuzz targets across 10+ packages.
  • Uses Go 1.18+ native fuzzing: func FuzzFoo(f *testing.F), f.Add(seed...), f.Fuzz(func(t *testing.T, in []byte) {...}).
  • Corpus seeds live in testdata/fuzz/. The fuzzer continuously mutates inputs; without the -fuzz flag, go test runs only the seed corpus (regression mode).
  • Notable targets: archive/tar, archive/zip, image/jpeg, image/png, internal/zstd, internal/runtime/maps (hash map implementation).

Parallel tests#

  • 797 t.Parallel() calls. Tests that do not mutate global state call t.Parallel() immediately to allow the test binary to run them concurrently. Guarded by testenv.MustHaveParallelism(t) on single-CPU systems.

Integration tests — the script test system#

The most distinctive testing pattern in this codebase. cmd/go has 916 .txt script test files in testdata/script/. cmd/compile and cmd/link have their own script suites.

Mechanism: Each .txt file is a txtar archive containing:

  1. Script commands at the top (a miniature shell DSL: go build, go test, ! stderr ., env GOPATH=...)
  2. Embedded source files in -- filename -- sections
# src/cmd/go/testdata/script/work_vendor_main_module_replaced.txt
go work vendor
go list all         # consistency checks pass
! stderr .

! go list all       # consistency checks fail after edit
stderr 'example.com/b@v0.0.0: is marked as replaced in vendor/modules.txt'

-- go.work --
go 1.21
use (a b)
-- a/go.mod --
module example.com/a

The script engine (cmd/internal/script) executes real go tool binaries inside a temporary module directory, making these true end-to-end tests. TestScript in cmd/go/script_test.go discovers all .txt files and runs each as a t.Run sub-test with t.Parallel(). The compiler version additionally replaces the installed compile binary with the test binary via TestMain, so script tests exercise the binary under test, not a stale installed version.

Separation from unit tests: No separate build tags needed. The script tests run as a normal go test sub-test, gated by testenv.MustHaveGoBuild(t) and testenv.SkipIfShortAndSlow(t).

testing.Short() gating#

  • The entire project uses testing.Short() (via testenv.SkipIfShortAndSlow(t)) to distinguish fast unit tests from slow integration tests. No -tags integration build tags are used; instead, -test.short prunes slow tests at runtime.

The export_test.go white-box bridge#

  • 44 files expose internal symbols from production packages specifically for test packages
  • Runtime’s export_test.go exposes soft-float functions, GC internals, and scheduler hooks
  • Used to test the runtime, strings, sync, net, crypto packages without weakening encapsulation

Test quality observations#

What’s done well#

  1. Absolute stdlib purity. Zero test dependencies on third-party libraries. Every assertion is via t.Errorf, t.Fatal, or t.Log. This is not incidental — the Go project is dogfooding its own testing package and proving it sufficient. The result is tests with no transitive dependency graph to manage.

  2. The script test system is a major quality investment. 916+ script tests for cmd/go provide coverage that pure unit tests cannot: real module resolution, real network caching (mocked via vcstest.NewServer()), real filesystem interactions. The txtar format makes each test self-contained and readable as a specification.

  3. First-class testing sub-packages. testing/fstest, testing/iotest, testing/slogtest, testing/synctest treat test infrastructure as a shipped library, not an afterthought. Any third-party package implementing fs.FS can use testfs.TestFS to validate conformance.

  4. internal/testenv as capability contract. Instead of ad-hoc if runtime.GOOS == "windows" checks scattered throughout tests, all capability checks funnel through named, discoverable functions. SkipFlaky(t, issue) even links to the bug tracker, making flaky test tracking systematic.

  5. Fuzz testing as regression suite. Fuzz corpus seeds in testdata/fuzz/ act as a permanent regression set. Any input that previously crashed the parser is now a test case automatically.

  6. Benchmarks as documentation of performance contracts. 1,944 benchmark functions serve dual purpose: they measure performance, but they also document which operations are performance-sensitive. BenchmarkIndex, BenchmarkSplit in strings communicate that these must be fast.

  7. The export_test.go pattern elegantly resolves black-box vs. white-box. External _test package is the default; export_test.go is the escape valve. This prevents test-only symbols from polluting the public API.

What could improve#

  1. Test coverage is uneven. The compiler’s SSA backend and runtime’s GC have fewer unit tests relative to their complexity — they rely on the integration test path (compiling real programs). Precise invariant testing of the GC write barrier, for instance, is difficult without internal exposure.

  2. testing/quick is frozen. The property-based testing package (testing/quick) has been frozen since Go 1.0-era and is not accepting new features. The native fuzzer (testing.F) partially replaces it, but quick.Check still has no ergonomic successor for structured random testing of pure functions.

  3. No standard conformance testing pattern for goroutine leak detection. Unlike testing/fstest’s TestFS, there is no goroutine.CheckLeak in the stdlib. Individual packages implement ad-hoc goroutine leak detection using runtime stack introspection in cleanup hooks.

Patterns worth emulating#

  • Script tests for CLI tools (cmd/internal/script + txtar) — any CLI tool with complex stateful behavior (file system, network, subprocess) should use this pattern instead of extensive unit-test mocking.
  • export_test.go for controlled white-box testing — a clean alternative to making internal types/functions public or relying on reflection.
  • internal/testenv-style capability guards — centralizing platform and build-capability checks into named functions prevents test fragility from scattered GOOS/GOARCH conditionals.
  • Fuzz corpus as regression archive — treating fuzz-discovered inputs as committed test cases in testdata/fuzz/ gives the benefits of property-based testing with the reproducibility of snapshot tests.
  • testing/synctest.Run for deterministic concurrency tests (Go 1.24+) — avoids time.Sleep in concurrent tests entirely by making goroutine scheduling and timers fully deterministic under a fake clock.