TestMain — Specification¶
This page collects the normative bits — package godoc, signature rules, exit-code semantics — that govern TestMain. Treat it as a reference, not a tutorial. Each section can be quoted in code review when you need to settle an argument about what is and is not guaranteed.
Signature (from src/testing/testing.go)¶
// TestMain is an optional function that may be defined in a _test.go file
// in a package. If it is defined, the generated test binary will call it
// instead of running the tests directly. TestMain runs in the main goroutine
// and can do whatever setup and teardown is necessary around a call to m.Run.
// It should then call os.Exit with the result of m.Run. When TestMain is
// called, flag.Parse has not been run. If TestMain depends on command-line
// flags, including those of the testing package, it should call flag.Parse
// explicitly.
//
// func TestMain(m *testing.M) {
// // ... setup ...
// code := m.Run()
// // ... teardown ...
// os.Exit(code)
// }
//
// As of Go 1.15, if TestMain returns normally (does not call os.Exit), the
// test will exit with the result returned from m.Run.
func TestMain(m *testing.M)
The signature is fixed. func TestMain(m *testing.M) — exactly one parameter, no return value, named TestMain, declared in a _test.go file.
The testing.M type¶
M.Run runs all registered tests, benchmarks, examples, and fuzz targets and returns 0 on success, 1 on failure. Exactly one call per process is supported.
The *testing.M value is constructed by the generated main via testing.MainStart:
func MainStart(deps testDeps, tests []InternalTest, benchmarks []InternalBenchmark,
fuzzTargets []InternalFuzzTarget, examples []InternalExample) *M
User code does not call MainStart. The Go tool inserts the call into the generated test main.
Build rules¶
- One
TestMainper package. Two declarations causeduplicate function TestMainat compile. TestMainlives in a_test.gofile. Putting it in a regular.gofile makes it a normal main function for the production binary, which is almost always a bug.- The package may be either
mypkg(internal) ormypkg_test(external). Pick the one that owns the setup; do not splitTestMainacross both. TestMainmust have the exact signaturefunc TestMain(m *testing.M). A mismatch is silently treated as either a regularTest*function (if it starts withTest) or as unused code.
Lifecycle¶
- Test binary
main()(generated bygo test) runs. - If
TestMainis defined, it is called with a freshly constructed*testing.M. Otherwisem.Run()runs directly. flag.Parse()has not been called. If you need-short,-v, or your own flags, callflag.Parse()yourself.- You call
m.Run(). It returns 0 (pass) or 1 (fail). - You exit.
os.Exit(code)is conventional; since Go 1.15 returning normally exits with them.Run()result.
The lifecycle is single-threaded with respect to test bodies: setup runs to completion before any test starts, and teardown runs after every test has finished. Within m.Run, tests may run in parallel if they call t.Parallel.
Exit-code semantics¶
m.Run() returns: - 0 — all tests passed. - 1 — at least one test failed, panicked, or the binary failed to initialize.
Other exit codes (2 for usage error) come from the flag package, not m.Run.
If TestMain calls os.Exit(n) directly, the process exits with n. The go test tool maps any non-zero exit code to "FAIL".
If TestMain returns normally (Go 1.15+), the process exits with the value returned by m.Run(). If m.Run was not called, returning normally exits with 0, which is wrong — always call m.Run.
os.Exit and deferred functions¶
os.Exit does not run deferred functions. If you write:
cleanup will not run. The fix is to capture the code, run teardown explicitly, then exit:
Or, since Go 1.15, return normally:
Note that returning normally loses the explicit pass-through of the failure code in older code reading habits, but the Go 1.15+ runtime forwards it for you.
The cleanest pattern preserves both defer and explicit exit:
func TestMain(m *testing.M) {
os.Exit(run(m))
}
func run(m *testing.M) int {
defer cleanup()
return m.Run()
}
run returns normally so defer fires, then os.Exit propagates the code.
Coverage and other hooks¶
testing.RegisterCoverwas the pre-Go 1.20 way the test binary registered coverage counters. As of Go 1.20 the cover tool emits a different runtime hook (runtime/coverage). User code generally does not callRegisterCoverdirectly.runtime.GC()may be called beforeos.Exitto force finalizers if your test verifies finalizer behavior. Rarely needed.runtime/coverageexposesWriteCounters(io.Writer)andWriteMeta(io.Writer)for tooling that wants to emit coverage data programmatically.
Flag-parsing rule¶
The package documentation is explicit: flag.Parse has not been called when TestMain is entered. You must call it before reading flag.Lookup, flag.Args, or testing.Short. Calling m.Run() itself triggers flag.Parse internally as a safety net, so for test code the flags are usable; but if TestMain reads a flag before m.Run, parse explicitly.
Custom flags must be defined as package-level variables in _test.go files via the flag package's String, Bool, Int, Var, etc. functions. Defining flags after flag.Parse is a runtime error in the form of a flag that is not recognized on the command line.
Concurrency¶
TestMain runs in the main goroutine. Test functions (TestXxx, BenchmarkXxx) run in goroutines spawned by m.Run. State set up in TestMain is visible to all tests through normal happens-before rules (initialization plus m.Run start).
Goroutines spawned in TestMain that outlive m.Run are not waited for. The process exits via os.Exit, which terminates all goroutines abruptly. This is technically not a "leak" because the process is ending, but tools like goleak may flag it.
Order of operations versus init¶
The init functions of all imported packages, including the package under test, run before TestMain. Any registration (sql.Register, flag.Var, prometheus.MustRegister) is complete by the time TestMain begins. You can therefore depend on init side effects in TestMain setup code.
The order is: 1. Package-level variable initialization (including init functions). 2. TestMain(m) is called. 3. m.Run() invokes individual TestXxx, BenchmarkXxx, etc.
testing.Main (legacy)¶
func Main(matchString MatchStringFunc, tests []InternalTest,
benchmarks []InternalBenchmark, examples []InternalExample)
testing.Main is the legacy entry point used by go test-generated main functions. User code does not call it. Stick to M.Run. The only reason to know testing.Main exists is to recognize it in old code or in code generators that emit test binaries.
Version history¶
- Go 1.4:
TestMainintroduced. - Go 1.7: subtests via
t.Run, no change toTestMainsemantics. - Go 1.14:
t.Cleanupintroduced as a complement toTestMain-style teardown. - Go 1.15:
TestMainmay return normally; the runtime exits withm.Run's result. - Go 1.18: fuzz targets become part of
m.Run's walk. - Go 1.20: coverage runtime overhauled;
runtime/coveragepackage introduced. - Go 1.21: no
TestMain-specific changes.
References¶
go doc testing.Mgo doc testing.M.Rungo doc testing.Main(the lower-level entry point; rarely called directly)go doc testing.MainStart- Go release notes for 1.15 (TestMain may return normally), 1.20 (coverage runtime overhaul).
src/testing/testing.goin the Go source tree.src/cmd/go/internal/test/test.goin the Go source tree for the generatedmain.
Reading the source¶
If you want to see the generated test main, run:
The -work flag preserves the temporary build directory, which contains _testmain.go. Open it; you will see the testing.MainStart call and your TestMain invocation.
This is the fastest way to settle any "but does m.Run really call my TestMain?" debate.
Comparison with other languages¶
The TestMain pattern is unusual among testing frameworks. For reference:
- JUnit (Java):
@BeforeAllstatic method runs once per class;@AfterAllruns once after all tests in the class. - pytest (Python):
conftest.pydefines session-scoped fixtures with@pytest.fixture(scope="session"); setup/teardown are baked into the fixture API. - Jest (JavaScript):
globalSetupandglobalTeardownconfigured injest.config.jsrun once per worker process. - Rust (
cargo test): No direct equivalent; one workaround is#[ctor]to run code at binary start.
Go's TestMain is unique in being a regular function that you write yourself, with the same syntax as any other function. There is no annotation, no DSL, no config file. The trade-off: you must manually wire m.Run and os.Exit. Most Go developers prefer this simplicity over the magic of attribute-based fixtures.
The MainStart signature¶
For completeness, the full MainStart signature:
func MainStart(deps testDeps, tests []InternalTest, benchmarks []InternalBenchmark,
fuzzTargets []InternalFuzzTarget, examples []InternalExample) *M
The testDeps interface is implemented by testing/internal/testdeps.TestDeps, which provides hooks for coverage, fuzzing infrastructure, and pattern matching. It is internal to the standard library.
The InternalTest type:
The test compiler emits a slice of these from your TestXxx functions. By the time MainStart returns, the *M knows about every test by name and function pointer.
What M.Run actually does¶
Roughly:
- Calls
flag.Parse()if it has not been called. - Filters tests by
-test.runregex. - For each test that matched: constructs a
*testing.T, calls the test function (in a goroutine to supportt.Parallel), waits for it to complete or fail. - Aggregates a pass/fail counter.
- Emits the
=== RUN/--- PASS/--- FAILlines. - Returns
0if all matched tests passed,1otherwise.
You do not need to know the details; the contract is sufficient. But knowing the shape demystifies behavior that otherwise looks like magic.
Final summary¶
TestMain is a small interface: one function, one method on *testing.M, three rules (one per package, _test.go file only, exact signature). The complexity lives in operational practice — what setup to do, how to tear down without defer traps, how to share state, how to integrate with CI. The other pages in this subsection cover that complexity in depth. Use this page as the authoritative reference for what the language and library guarantee.
Exit-code matrix¶
A quick table of what happens with various combinations:
What TestMain does | What m.Run returns | What TestMain calls | Exit code |
|---|---|---|---|
Calls m.Run, then os.Exit(code) | 0 (pass) | os.Exit(0) | 0 |
Calls m.Run, then os.Exit(code) | 1 (fail) | os.Exit(1) | 1 |
Calls m.Run, returns normally (Go 1.15+) | 0 | (return) | 0 |
Calls m.Run, returns normally (Go 1.15+) | 1 | (return) | 1 |
Calls m.Run, ignores result, returns normally | 0 or 1 | (return) | 0 (bug!) |
Does not call m.Run, returns normally | n/a | (return) | 0 |
| Panics | n/a | (panic) | 2 (Go's panic exit code) |
Calls os.Exit(1) before m.Run | n/a | os.Exit(1) | 1 |
The row to remember: "ignores result, returns normally" — the silent CI-success bug.
Quoted godoc surface¶
For complete reference, here are the related godoc entries you may consult:
testing.M— opaque struct, holds test/benchmark/example metadata.testing.M.Run() int— runs all registered tests, returns exit code.testing.Short() bool— true when-shortwas passed andflag.Parsehas run.testing.Verbose() bool— true when-vwas passed andflag.Parsehas run.testing.MainStart(deps, tests, benches, fuzzes, examples) *M— constructs*M; called by generated test main.testing.RegisterCover(c Cover)— legacy coverage registration; not used in Go 1.20+.testing.AllocsPerRun(runs int, f func()) float64— utility, unrelated toTestMainbut lives in the same package.
Refer to go doc testing for the full surface. The M type is intentionally minimal; almost all functionality lives on *T and *B, not on *M.
Notes on go test flag pass-through¶
When you run go test -someflag ./..., the Go tool inspects -someflag:
- If it is a known
go testflag (-v,-run,-short, etc.), it is mapped to the corresponding-test.someflagflag of the binary. - If it is unknown, it is passed through to the binary verbatim. Your custom flags work this way.
- Some flags (
-c,-o,-i) arego testcommands, not binary flags; they affect compilation, not runtime.
This means the binary built by go test -c accepts flags using the -test.* prefix for built-in flags but the unprefixed name for your custom flags:
This is occasionally relevant when debugging under dlv or when running test binaries directly.
When m.Run panics¶
Theoretically, m.Run could panic — e.g., due to a runtime bug or a defect in the testing package. In practice this never happens; tests that panic are caught inside m.Run and converted to failures. But if you want belt-and-suspenders, wrap m.Run in recover. Most production code does not bother.