Skip to content

Instantly share code, notes, and snippets.

@Integralist
Last active May 24, 2026 13:26
Show Gist options
  • Select an option

  • Save Integralist/97977fd11049213985c8fa4bd2712614 to your computer and use it in GitHub Desktop.

Select an option

Save Integralist/97977fd11049213985c8fa4bd2712614 to your computer and use it in GitHub Desktop.
Go: Discoverable Test Config in Go with Flags (rednafi.com)

Discoverable Test Config in Go: Use Flags

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.

1. Build tags — discouraged

//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.

2. Environment variables — better, still limited

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.

3. Custom flags — recommended

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.

Notes

  • Built-in test flags show with a test. prefix in -h but invoked without it; custom flags use exactly what you register.
  • Namespace custom flags (e.g. custom.snapshot) for greppability.
  • TestMain for package-wide setup; init() for file-local flags. Author prefers file-level.

Conclusion

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.


Aside: what is TestMain?

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.

What each line does

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() — reads os.Args and populates any flag.Bool(...)/flag.String(...) variables you declared. Without this, your custom flags stay at their defaults.
  • m.Run() — runs all the TestXxx functions and returns an exit code (0 = pass, 1 = fail).
  • os.Exit(...) — terminates the process with that code. You must use os.Exit rather than return; historically m.Run() didn't call exit itself, so you own it.

Why you'd use it

  1. 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)
    }
  2. Register custom flags so flag.Parse() sees them before tests run (this article's case).
  3. Global config — logging, env setup, etc.

Why the article uses it

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment