16.4 Testing Code
In many languages the test framework comes from the community, and different projects may use
different frameworks, assertion libraries and runners; before running the tests of an unfamiliar
project, one often has to work out which set it uses. Go puts testing in the toolchain: go test is
a subcommand of the go command, testing is a package in the standard library, and only a few
naming conventions connect the two. This section first explains how go test organizes tests into
an executable program according to those conventions. It then discusses the design choices of the
testing package: why the reporting primitives are so plain, how subtests solve parameterization
and parallelism together, and how fuzzing brings coverage-guided input generation under the same
conventions. Finally it outlines the additions made to the testing package in recent releases.
16.4.1 From Conventions to an Executable Program
go test discovers tests from file names and function signatures: a file ending in _test.go is
compiled only when testing, and within it a function of the form func TestXxx(*testing.T) is a
test, func BenchmarkXxx(*testing.B) a benchmark, func FuzzXxx(*testing.F) a fuzz test, and
func ExampleXxx() an example; if an example function ends with a comment beginning with Output:,
the function’s standard output is compared against it at run time
[Go Authors, n.d.a]. There is no configuration file and no annotation
[Go Authors, n.d.d]. Below is a function that reverses a string byte by byte,
together with its tests; the examples in this section all use it as the subject. A byte-wise
Reverse is also the example used by the official fuzzing tutorial
[Go Authors, n.d.e]:
| |
| |
How do the conventions become a runnable program? go test generates a main package for the
package under test, registers the test functions it found in tables, and links it together with the
package under test and the test files into a test binary, which it then runs. The template for the
generated code is in cmd/go/internal/load/test.go [Go Authors, 2026a];
running go test -work with Go 1.26.10 shows the files generated for the package above in the work
directory, here with the blank lines left by the template removed:
| |
If the test package defines func TestMain(m *testing.M), the generated main calls it instead,
and it performs global setup and teardown before and after m.Run()
[Go Authors, n.d.a]. When building the test binary, go test also runs a
subset of the higher-confidence go vet checks on the package under test and does not run the tests
if they report a problem. In package-list mode it caches successful test results: when the test
binary, the command line, and the files and environment variables read by the test are all
unchanged, it prints the previous result directly and marks it (cached). Both behaviors date from
Go 1.10 [Go Authors, 2018; Go Authors, 2026b].
16.4.2 Plain Reporting Primitives
The testing package has no assertion functions, only a few reporting primitives: t.Error and
t.Errorf record a failure and let the test continue; t.Fatal and t.Fatalf record a failure and
then call FailNow, which ends the current goroutine through runtime.Goexit, runs all its
deferred calls, and moves on to the next test. For this reason FailNow must be called in the
goroutine that runs the test function, not in other goroutines the test creates, and it does not
stop those goroutines [Go Authors, 2026c].
Omitting assertions is a deliberate choice. The reasoning in the Go FAQ is that good error handling
means that when one check fails the others keep running, so the person debugging sees the full
picture of the failure, for example that isPrime gives wrong answers for 2, 3, 5 and 7 rather than
only that it fails on 2; and test frameworks tend to grow into small languages with their own
conditionals, control flow and printing, which Go already has
[Go Authors, n.d.f]. The Go test review comments advise against assertion
libraries more bluntly: assertions either abort the test early or discard the information about what
the test did right, and they force the assertion package to invent a sub-language instead of reusing
Go itself [Go Authors, n.d.c]. The same document recommends that failure
messages give the actual value before the expected one, usually in the format
YourFunc(%v) = %v, want %v; the pair of names got and want has thus become an idiom of Go
tests. The failure message in TestReverse above uses this format. The price is more boilerplate
than a one-line assertion, which is why third-party assertion libraries such as testify have many
users.
Helper functions raise a concrete problem: when a helper calls t.Error, the line number in the
failure report points inside the helper rather than at the test that called it, which is of almost
no help in locating the failure. t.Helper, added in Go 1.9, marks the caller as a helper function,
and the reporting of positions skips these frames and gives the position in the first non-helper
function [Spare, 2016].
16.4.3 Table-Driven Tests and Subtests
When failure messages have to be written by hand one at a time, writing the inputs and expectations
as a table and checking row by row in a loop spreads the cost of writing a good message over all the
cases, which is what the Go FAQ recommends [Go Authors, n.d.f]. This style is
known in the Go community as table-driven testing [Cheney, 2019]. Early
table-driven tests had two inconveniences: a single row of the table could not be run on its own,
and the failure message had to carry the name of the case itself. Subtests, proposed by van Lohuizen
in 2015, solved these problems; the motivations listed include selecting a single table case from
the command line, using methods such as Fail within a subtest, controlling the scope of setup and
teardown at a finer granularity than TestMain, and better control over parallelism
[van Lohuizen, 2015]. Subtests shipped with Go 1.7
[Go Authors, 2016]. t.Run runs the subtest in a new goroutine and returns
only when the subtest returns or calls t.Parallel; the name of a subtest is appended to its
parent’s after a slash, and the patterns of -run are likewise matched slash-separated segment by
segment [Go Authors, 2026c]. Run with Go 1.26.10, with the durations
removed:
| |
The output shows how parallel subtests are scheduled: each subtest pauses (PAUSE) after calling
t.Parallel, t.Run returns at once, and the loop goes on to create the next subtest; only after
the body of the parent test finishes do the paused subtests resume together (CONT). The parent
test is not complete until all its subtests finish, so a group of parallel subtests runs in parallel
only with one another [Go Authors, n.d.a]. To run just one row, use
go test -run TestReverse/ascii.
This scheduling, together with the semantics of the for loop, once formed a common trap. Before Go
1.22, a variable declared by a for statement was created once and updated on each iteration
[Go Authors, 2024]; the parallel subtests ran only after the loop ended, the
tt captured by the closure was already the last row, and all the subtests actually checked the
same case, which passed anyway. The idiom of the time was to add the line tt := tt before the
closure. Go 1.22 creates a new variable for each iteration. The change takes effect per file by
language version: the new semantics apply only when the version determined by the go directive in
go.mod or by build constraints is Go 1.22 or later [Go Authors, 2026d].
The module above declares go 1.26, so that line is not needed.
Beyond subtests, *testing.T has gradually gained methods for managing test resources: t.Cleanup
registers a cleanup function that runs after the test and its subtests finish (Go 1.14); t.TempDir
returns a temporary directory that is deleted automatically after the test (Go 1.15); t.Setenv
sets an environment variable for the duration of the test (Go 1.17); t.Context returns a context
that is canceled before the cleanup functions run, and t.Chdir changes the working directory for
the duration of the test (Go 1.24) [Go Authors, 2026c;
Go Authors, 2025a]. What they have in common is that they bind the lifetime
of a resource to the test. t.Setenv and t.Chdir, which modify process-wide state, cannot be used
together with t.Parallel [Go Authors, 2026c].
16.4.4 Fuzzing
Table-driven tests can only check inputs the author thought of. All of TestReverse above passes,
but its three cases are ASCII strings, for which byte-wise reversal happens to be correct.
Fuzzing takes a different approach: the author writes only a property that should hold for all
inputs, and the tool generates large numbers of inputs to look for a counterexample. The property
checked by FuzzReverse is that reversing a valid UTF-8 string should yield a valid UTF-8 string.
Without -fuzz, go test just runs the seeds supplied by f.Add and the existing corpus under the
testdata/fuzz/FuzzReverse directory as ordinary cases; only with -fuzz does it enter fuzzing
mode [Go Authors, n.d.b]. One run with Go 1.26.10:
| |
After less than a second and a little over six hundred executions, the engine found a
counterexample: Δ takes two bytes in UTF-8, and byte-wise reversal splits it. The exact input
found differs from run to run. The engine first measures baseline coverage with the seed corpus,
then mutates inputs in several worker processes and keeps those that increase coverage
(new interesting in the output). This generated corpus is stored in $GOCACHE/fuzz; the input
that caused the failure is written to testdata/fuzz/FuzzReverse, and once it is committed with the
code, every later ordinary go test runs it, so the bug becomes a permanent regression case
[Go Authors, n.d.b; Go Authors, 2022]. Coverage
guidance depends on compiler instrumentation, and the documentation recommends running fuzz tests on
platforms that support instrumentation, currently amd64 and arm64
[Go Authors, n.d.b].
Go’s fuzzing combines two streams. One is coverage-guided fuzzing: Hockman’s design draft takes AFL
and libFuzzer as references and notes that tools already in the Go community, such as go-fuzz, had
successfully found a large number of bugs but lacked native integration with the toolchain
[Hockman, 2020]. The other is property-based random testing, which originates
with Claessen and Hughes’s QuickCheck: instead of writing concrete cases, one writes properties that
should always hold, and the tool generates random inputs to try to refute them
[Claessen and Hughes, 2000]. The standard library’s testing/quick
belongs to this line, and its documentation states that the package is frozen and accepts no new
features [Go Authors, 2026e]. FuzzReverse combines both: it asserts a
property, and coverage feedback drives the generation of inputs. The draft was finalized as proposal
#44551, and fuzzing shipped with Go 1.18 [Go Authors, 2022].
16.4.5 Benchmarks and Concurrent Tests
Benchmarks use the same conventions, with functions of the form func BenchmarkXxx(b *testing.B).
b.Loop, introduced in Go 1.24, replaces the traditional b.N loop: the benchmark function runs
only once per -count, and costly setup and teardown also run only once; the arguments and results
of function calls are kept alive, so the compiler cannot optimize the whole loop body away
[Go Authors, 2025a]. How b.N converges, how b.Loop is implemented, and
memory measurement are covered in 16.5.
Testing concurrent code has two dedicated tools. When tests are run with -race, the race detector
reports the data races the tests actually exercise (16.2). testing/synctest, which
became a regular feature in Go 1.25, runs a test in an isolated “bubble”; time inside the bubble is
virtual, and the clock advances immediately when all goroutines are durably blocked, so tests that
depend on timeouts and timers are both fast and deterministic
[Go Authors, 2025b]. Its detection of deadlocks inside a bubble is covered
in 16.1.4.
16.4.6 Recent Additions
The interfaces added to the testing package in the last few releases mostly serve to connect tests
with external systems [Go Authors, 2025a;
Go Authors, 2025b; Go Authors, 2026f]:
| Version | Added | Purpose |
|---|---|---|
| Go 1.24 | T.Context, T.Chdir, B.Loop | A context and working directory bound to the test lifecycle; the new benchmark loop |
| Go 1.25 | T.Attr, T.Output, testing/synctest | Emit key-value attributes to the test log, with their meaning left to continuous integration systems and test frameworks; an io.Writer on the same output stream as t.Log; virtual time for concurrent tests |
| Go 1.26 | T.ArtifactDir, testing/cryptotest.SetGlobalRandom | A directory for saving test artifacts; a deterministic random source for cryptographic functions |
When go test -artifacts is given, the directory returned by ArtifactDir is under the output
directory and can be inspected afterwards; otherwise it is a temporary directory deleted after the
test [Go Authors, 2026f]. SetGlobalRandom appeared because of another Go
1.26 change: functions in the crypto packages, such as key generation, no longer use a random
source passed in by the caller but always use secure random bytes, so tests that need determinism
use this function instead [Go Authors, 2026f]. The versions in which the
interfaces in the table were introduced agree with the records in api/go1.24.txt through
api/go1.26.txt.
16.4.7 Summary
Go’s testing rests on a few naming conventions: go test discovers tests by the conventions and
generates a main package that organizes them into a test binary. The testing package is
deliberately plain. It provides only reporting primitives, leaves the responsibility of expressing
failures to the test author, and spreads that cost with table-driven tests; subtests let each row of
the table be run on its own and in parallel; fuzzing brings coverage-guided input generation under
the same conventions, and the counterexamples it finds become regression cases in the form of corpus
files.
This design also gives something up. Conveniences such as assertions and mock objects are left to third-party libraries or boilerplate, and the rules for discovering tests cannot be redefined; in return, all Go projects organize and run tests the same way, and continuous integration, coverage tools and editors need to face only one set of conventions. This is consistent with Go’s trade-offs elsewhere (1.1): a simple convention that everyone understands and tools can recognize buys low friction in collaboration across the whole ecosystem.
Further Reading
- [Cheney, 2019] Dave Cheney. Prefer table driven tests. 2019. https://dave.cheney.net/2019/05/07/prefer-table-driven-tests
- [Claessen and Hughes, 2000] Koen Claessen and John Hughes. QuickCheck: A Lightweight Tool for Random Testing of Haskell Programs. In Proceedings of the Fifth ACM SIGPLAN International Conference on Functional Programming (ICFP ‘00), 2000. https://doi.org/10.1145/351240.351266 The origin of property-based random testing.
- [Go Authors, n.d.a] The Go Authors. Package testing.
https://pkg.go.dev/testing
The complete API and documentation of
T,BandF. - [Go Authors, n.d.b] The Go Authors. Go Fuzzing. https://go.dev/doc/security/fuzz/ Usage of native fuzzing, the corpus directory convention and platform requirements.
- [Go Authors, n.d.c] The Go Authors. Go Test Comments (wiki).
https://go.dev/wiki/TestComments The
YourFunc(%v) = %v, want %vfailure message format and the rationale for avoiding assertion libraries. - [Go Authors, n.d.d] The Go Authors. Tutorial: Add a test. https://go.dev/doc/tutorial/add-a-test
- [Go Authors, n.d.e] The Go Authors. Tutorial: Getting started with
fuzzing. https://go.dev/doc/tutorial/fuzz
A fuzzing tutorial that uses
Reverseas its example. - [Go Authors, n.d.f] The Go Authors. Frequently Asked Questions (FAQ): Where is my favorite helper function for testing? https://go.dev/doc/faq#testing_framework
- [Go Authors, 2016] The Go Authors. Go 1.7 Release Notes. 2016. https://go.dev/doc/go1.7 Subtests and sub-benchmarks.
- [Go Authors, 2018] The Go Authors. Go 1.10 Release Notes. 2018.
https://go.dev/doc/go1.10
Test result caching;
go testrunsgo vetautomatically. - [Go Authors, 2022] The Go Authors. Go 1.18 Release Notes. 2022.
https://go.dev/doc/go1.18#fuzzing
Native fuzzing; the generated corpus is written to
$GOCACHE/fuzz. - [Go Authors, 2024] The Go Authors. Go 1.22 Release Notes. 2024.
https://go.dev/doc/go1.22
Each iteration of a
forloop creates new variables. - [Go Authors, 2025a] The Go Authors. Go 1.24 Release
Notes. 2025. https://go.dev/doc/go1.24
T.Context,T.Chdir,B.Loop. - [Go Authors, 2025b] The Go Authors. Go 1.25 Release
Notes. 2025. https://go.dev/doc/go1.25
T.Attr,T.Output;testing/synctestbecomes a regular feature. - [Go Authors, 2026a] The Go Authors.
src/cmd/go/internal/load/test.go:
testmainTmpl. Go 1.26 source. https://github.com/golang/go/blob/release-branch.go1.26/src/cmd/go/internal/load/test.go - [Go Authors, 2026b] The Go Authors.
src/cmd/go/internal/test/test.go. Go 1.26 source.
https://github.com/golang/go/blob/release-branch.go1.26/src/cmd/go/internal/test/test.go The
subset of
go vetchecks thatgo testruns automatically. - [Go Authors, 2026c] The Go Authors. src/testing/testing.go. Go 1.26 source. https://github.com/golang/go/blob/release-branch.go1.26/src/testing/testing.go
- [Go Authors, 2026d] The Go Authors.
src/cmd/compile/internal/noder/writer.go. Go 1.26 source.
https://github.com/golang/go/blob/release-branch.go1.26/src/cmd/compile/internal/noder/writer.go
The semantics of
forloop variables are decided by the per-file language version. - [Go Authors, 2026e] The Go Authors. src/testing/quick/quick.go. Go 1.26 source. https://github.com/golang/go/blob/release-branch.go1.26/src/testing/quick/quick.go
- [Go Authors, 2026f] The Go Authors. Go 1.26 Release
Notes. 2026. https://go.dev/doc/go1.26
T.ArtifactDirandgo test -artifacts;testing/cryptotest.SetGlobalRandom. - [Hockman, 2020] Katie Hockman. Design Draft: First Class Fuzzing. Go design draft, 2020. https://go.dev/design/draft-fuzzing Finalized as proposal #44551.
- [Spare, 2016] Caleb Spare. Proposal: testing: better support test helper functions with TB.Helper. Go design document, 2016-12-27. https://go.dev/design/4899-testing-helper Shipped with Go 1.9.
- [van Lohuizen, 2015] Marcel van Lohuizen. Proposal: testing: programmatic sub-test and sub-benchmark support. Go design document, 2015-09-02. https://go.dev/design/12166-subtests
- This book: 16.5 Benchmarking and Performance Profiling (the mechanics of
b.Loop,b.Nconvergence and memory measurement), 16.1 Runtime Deadlock Detection, 16.2 Race Detection.