In TestMain, what does the int returned by m.Run mean, and how must it reach go test?
answer
- it is a process exit status
- zero means everything selected passed
- go test reads the binary's exit code
- os.Exit(0) after m.Run is a permanent pass
- capture the code, tear down, then exit with it
basics
~20 sm.Run returns the test binary's exit code: zero if every selected test passed, non-zero otherwise. TestMain either passes it to os.Exit or simply returns, and the generated test main exits with it. Hardcoding os.Exit(0) hides real failures.
solid answer
~50 s`m.Run()` runs the selected tests and returns the status the process should exit with - zero when everything passed, non-zero when anything failed, panicked or the run was aborted. That number is how the test binary talks to `go test`, which reports the package as FAIL when the binary exits non-zero. Two shapes are correct: the long-standing `os.Exit(m.Run())`, or capturing `code := m.Run()`, doing teardown, and then `os.Exit(code)`; simply returning from TestMain also works, because the generated test main exits with the code `m.Run` recorded. What is never correct is inventing your own status - `m.Run(); os.Exit(0)` throws the verdict away and gives you a permanently green package. The same value is your abort channel during setup: if the shared listener will not bind, print the error to stderr and `os.Exit(1)` before calling `m.Run` at all.
code
go · 6 linesfunc TestMain(m *testing.M) {
startFixture()
m.Run() // return value dropped
stopFixture()
os.Exit(0) // always green, whatever the tests said
}go deeper
Remember that m.Run hands back a number that means pass or fail, and that it must be the number the process exits with. Never write os.Exit(0) after it.
Explain that go test decides the package verdict from the binary's exit status, and show the capture-teardown-exit ordering plus the os.Exit(1) abort path when shared setup fails.
Talk about detecting a disarmed package: deliberately break an assertion and confirm FAIL, and treat any hand-written TestMain as something to audit for a swallowed verdict.
Own the guarantee that a green pipeline means something. Decide whether infrastructure failure gets its own non-zero code, and how a suite that stops reporting failures would be noticed at all.
## The contract `func (m *testing.M) Run() int` does the work the framework would have done without TestMain: it parses flags if they are not parsed yet, selects tests, benchmarks, examples and fuzz targets according to `-run`, `-bench`, `-fuzz` and friends, runs them, prints their output, and returns **the exit status the process should report**. - **0** - everything selected passed (or was skipped). - **non-zero** - at least one selected test failed, or the run was aborted. That integer is the only verdict channel the test binary has. `go test` launches the binary and marks the package FAIL when it exits non-zero. A TestMain that loses the value has broken the reporting chain, and it breaks it in the worst possible direction: towards green. ## Three shapes, two of them correct ```go // Correct, and the most explicit. func TestMain(m *testing.M) { os.Exit(m.Run()) } // Correct, and the shape you need when teardown exists. func TestMain(m *testing.M) { setup() code := m.Run() teardown() os.Exit(code) } // Broken: the package can never fail. func TestMain(m *testing.M) { setup() m.Run() teardown() os.Exit(0) } ``` The third one is not hypothetical - it is what people write when they add teardown to the first shape and reach for a literal exit code to keep the compiler quiet about an unused value. There is a fourth shape that is also correct in supported Go releases: TestMain may just **return** without calling `os.Exit` at all, and the generated test main exits with the status `m.Run` recorded. That is convenient, but writing the exit explicitly is still the clearer signal of intent, and it is what most codebases contain. ## Not calling m.Run at all The extreme version of the same failure. If TestMain returns without ever calling `m.Run`, no test in the package runs, nothing fails, and the binary exits zero. `go test` prints `ok` for the package with a suspiciously short duration. Nothing in the toolchain flags it. The only real defence is noticing that the package's output no longer lists any test names under `-v`, or that its coverage collapsed. ## Using the code as an abort channel Setup in TestMain runs before any test exists, so there is no `*testing.T` to fail. The idiom is to report on standard error and exit non-zero yourself: ```go lis, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { fmt.Fprintln(os.Stderr, "listen:", err) os.Exit(1) } ``` Exiting 1 from failed setup is right: the package genuinely did not pass. Exiting 0 because "the tests never got a chance to fail" is how an unbuildable fixture turns into a green build. Some suites prefer a distinct non-zero code (say 2) for infrastructure failure so a CI job can tell "tests failed" from "the environment is broken" - anything non-zero is a failure to `go test`, and the distinction is only for your own tooling. ## Why teams get bitten The symptom is always the same and always late: a package that has not reported a failure in months, discovered when someone deliberately breaks a test and the build stays green. The check is fast - break an assertion on purpose, run the package, and confirm `go test` prints FAIL and `echo $?` is non-zero. Any package with a hand-written TestMain deserves that check once. ## Interaction with the rest of the run - The exit status covers the **whole package**, not individual tests; per-test results are in the output stream, not the code. - Teardown that runs after `m.Run` cannot change the verdict. If cleanup itself fails and you care, you must fold that into the code you exit with - for example, exit non-zero when the shared fixture could not be released. - Because `os.Exit` skips deferred functions, the placement of teardown relative to `m.Run` and `os.Exit` is not a style choice; it is the difference between cleanup running and not running.
- Setup in TestMain fails before any test runs. How do you report that?There is no `*testing.T` yet, so write the reason to `os.Stderr` and `os.Exit` with a non-zero status without calling `m.Run`. The package then fails, which is honest - it did not test anything. Exiting zero on failed setup turns a broken fixture into a green build.
- If teardown after m.Run fails, should the package fail?Usually yes, if the failure means the environment is now dirty - a listener or child process left alive will break the next run. Fold it into the status you exit with: keep m.Run's code, and replace a zero with a non-zero one when cleanup failed. Never let cleanup downgrade a real test failure to success.
- Does a non-zero exit code tell go test which test failed?No. The status is a single package-level verdict; the per-test detail lives in the binary's output, which `go test` streams and summarises. That is why a TestMain that swallows output or exits early can leave you with a FAIL and no visible reason.
saying these in an interview costs you the question
- Calls m.Run then os.Exit(0) unconditionally
- Thinks go test parses output rather than the exit status
- Believes an unused m.Run result is a compile error
- Exits zero when shared setup failed
- Lets teardown overwrite a real test failure with success