Skip to content

TestMain — Specification

← Back

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

type M struct {
    // unexported fields
}

func (m *M) Run() (code int)

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 TestMain per package. Two declarations cause duplicate function TestMain at compile.
  • TestMain lives in a _test.go file. Putting it in a regular .go file makes it a normal main function for the production binary, which is almost always a bug.
  • The package may be either mypkg (internal) or mypkg_test (external). Pick the one that owns the setup; do not split TestMain across both.
  • TestMain must have the exact signature func TestMain(m *testing.M). A mismatch is silently treated as either a regular Test* function (if it starts with Test) or as unused code.

Lifecycle

  1. Test binary main() (generated by go test) runs.
  2. If TestMain is defined, it is called with a freshly constructed *testing.M. Otherwise m.Run() runs directly.
  3. flag.Parse() has not been called. If you need -short, -v, or your own flags, call flag.Parse() yourself.
  4. You call m.Run(). It returns 0 (pass) or 1 (fail).
  5. You exit. os.Exit(code) is conventional; since Go 1.15 returning normally exits with the m.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:

defer cleanup()
os.Exit(m.Run())

cleanup will not run. The fix is to capture the code, run teardown explicitly, then exit:

code := m.Run()
cleanup()
os.Exit(code)

Or, since Go 1.15, return normally:

defer cleanup()
m.Run()

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.RegisterCover was 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 call RegisterCover directly.
  • runtime.GC() may be called before os.Exit to force finalizers if your test verifies finalizer behavior. Rarely needed.
  • runtime/coverage exposes WriteCounters(io.Writer) and WriteMeta(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: TestMain introduced.
  • Go 1.7: subtests via t.Run, no change to TestMain semantics.
  • Go 1.14: t.Cleanup introduced as a complement to TestMain-style teardown.
  • Go 1.15: TestMain may return normally; the runtime exits with m.Run's result.
  • Go 1.18: fuzz targets become part of m.Run's walk.
  • Go 1.20: coverage runtime overhauled; runtime/coverage package introduced.
  • Go 1.21: no TestMain-specific changes.

References

  • go doc testing.M
  • go doc testing.M.Run
  • go 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.go in the Go source tree.
  • src/cmd/go/internal/test/test.go in the Go source tree for the generated main.

Reading the source

If you want to see the generated test main, run:

go test -work -c -o /dev/null ./yourpkg

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): @BeforeAll static method runs once per class; @AfterAll runs once after all tests in the class.
  • pytest (Python): conftest.py defines session-scoped fixtures with @pytest.fixture(scope="session"); setup/teardown are baked into the fixture API.
  • Jest (JavaScript): globalSetup and globalTeardown configured in jest.config.js run 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:

type InternalTest struct {
    Name string
    F    func(*T)
}

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:

  1. Calls flag.Parse() if it has not been called.
  2. Filters tests by -test.run regex.
  3. For each test that matched: constructs a *testing.T, calls the test function (in a goroutine to support t.Parallel), waits for it to complete or fail.
  4. Aggregates a pass/fail counter.
  5. Emits the === RUN / --- PASS / --- FAIL lines.
  6. Returns 0 if all matched tests passed, 1 otherwise.

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 -short was passed and flag.Parse has run.
  • testing.Verbose() bool — true when -v was passed and flag.Parse has 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 to TestMain but 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 test flag (-v, -run, -short, etc.), it is mapped to the corresponding -test.someflag flag 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) are go test commands, 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:

./mypkg.test -test.v -test.run TestSum -dburl=memory://

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.