Summary of rednafi.com/go/test-config-with-flags.
The article compares three ways to toggle tests in Go (snapshot, integration, real-vs-mock) and argues for custom flags.
//go:build snapshot
package main
import "testing"
func TestSnapshot(t *testing.T) {
t.Log("running snapshot")
}Run with go test -tags=snapshot. Not discoverable without grepping, applies per-file (not per-test), and multiplies poorly.
if os.Getenv("SNAPSHOT") != "1" {
t.Skip("set SNAPSHOT=1 to run this test")
}No recompile, self-documenting via skip messages. But checks hide inside helpers and silently alter behavior.
Centralized via TestMain:
var snapshot = flag.Bool("snapshot", false, "run snapshot tests")
func TestMain(m *testing.M) {
flag.Parse()
os.Exit(m.Run())
}File-local via init():
func init() {
flag.BoolVar(&snapshot, "snapshot", false, "run snapshot tests")
}Invoke: go test -v -snapshot. List all flags: go test -v -args -h.
- Built-in test flags show with a
test.prefix in-hbut invoked without it; custom flags use exactly what you register. - Namespace custom flags (e.g.
custom.snapshot) for greppability. TestMainfor package-wide setup;init()for file-local flags. Author prefers file-level.
Flags beat env vars and build tags: typed, explicit, discoverable via -h, and native to Go's tooling. Even env vars are best mapped onto flags. References Peter Bourgon's 2018 "industrial Go programming" post.
TestMain is an optional hook Go's testing package looks for. If you define it in a _test.go file, the test binary calls your TestMain instead of running tests directly — making it the entry point for the test binary in that package.
func TestMain(m *testing.M) {
flag.Parse() // parse command-line flags (yours + test framework's)
os.Exit(m.Run()) // actually run the tests, exit with their status code
}m *testing.M— a handle representing "the test suite for this package."flag.Parse()— readsos.Argsand populates anyflag.Bool(...)/flag.String(...)variables you declared. Without this, your custom flags stay at their defaults.m.Run()— runs all theTestXxxfunctions and returns an exit code (0 = pass, 1 = fail).os.Exit(...)— terminates the process with that code. You must useos.Exitrather thanreturn; historicallym.Run()didn't call exit itself, so you own it.
- Setup/teardown around the whole package — spin up a DB, seed fixtures, tear down after:
func TestMain(m *testing.M) { db := startTestDB() code := m.Run() db.Stop() os.Exit(code) }
- Register custom flags so
flag.Parse()sees them before tests run (this article's case). - Global config — logging, env setup, etc.
When you call go test -snapshot, the test binary needs to know -snapshot exists. Declaring var snapshot = flag.Bool("snapshot", ...) registers it; flag.Parse() inside TestMain reads the value before m.Run() runs the tests that check it.
Note: Go's test framework actually calls flag.Parse() itself before TestMain in modern versions, so the explicit flag.Parse() is often redundant — but harmless and makes intent clear. The init() alternative shown above avoids needing TestMain at all for simple flag registration.