skip to content

In TestMain, how do you define and read a custom test flag such as -dsn or -port?

level: middleimportance: nice to knowfreq 30%

answer

  1. registration and reading happen at different times
  2. package variables initialise before anything parses
  3. TestMain runs before m.Run would parse
  4. call flag.Parse first, then dereference
  5. prefer -name=value or put it after -args

basics

~20 s

Register the flag at package level in a _test.go file with flag.String or flag.Int, then call flag.Parse() inside TestMain before reading the value and before m.Run. go test passes flags it does not recognise through to the test binary.

solid answer

~40 s

Declare it as a package-level variable in a `_test.go` file - `var port = flag.Int("port", 0, "port for the shared listener")` - so registration happens during package initialisation, before anything parses. Then call `flag.Parse()` as the first thing in TestMain, because TestMain runs before `m.Run`, and `m.Run` is what would otherwise parse for you; if you dereference `*port` before parsing, you silently get the default. On the command line, `go test -port=9000 ./internal/transport` works because the go command forwards flags it does not recognise to the test binary; use the `-flag=value` form, or put everything after `-args`, so the go command never has to guess whether the flag takes a value. Always give a working default so a plain `go test ./...` still runs.

code

go · 13 lines
go
var port = flag.Int("port", 0, "port for the shared test listener; 0 picks a free one")

func TestMain(m *testing.M) {
	flag.Parse()
	lis, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", *port))
	if err != nil {
		fmt.Fprintln(os.Stderr, "listen:", err)
		os.Exit(1)
	}
	code := m.Run()
	lis.Close()
	os.Exit(code)
}

go deeper

for a junior

Know that a test binary can take its own flags: declare one with flag.String or flag.Int next to the tests, and read it only after flag.Parse has run.

for a middle

Explain the ordering - registration during package initialisation, TestMain before m.Run, and m.Run parsing only if nobody did - and why reading a flag at variable-initialisation time silently yields the default.

for a senior

Show how you keep the default path working: sane defaults so go test ./... needs no arguments, skipping instead of failing when an external dependency is not configured, and clear usage strings.

for a principal

Decide the convention across the codebase: which knobs are flags, which are environment variables injected by CI, and how a new engineer discovers that a suite has any at all.

## Why TestMain is involved at all A test binary is an ordinary Go program, so it can have its own command-line flags. What makes the timing awkward is that a test binary has three parties wanting to read the command line: the `go` command, the `testing` package (`-test.v`, `-test.run`, `-test.timeout` and the rest), and you. The ordering that matters is: 1. Package-level variables and `init` functions run. **This is where flags must be registered.** 2. The generated main creates the `*testing.M` and calls **TestMain**, if there is one. 3. `m.Run` parses flags if they have not been parsed yet, then runs the tests. Step 3 is the catch. Without TestMain, you never notice: parsing happens before any test body executes, so a test reading `*port` sees the real value. With TestMain, your setup code runs at step 2 - **before** anything has parsed - so it must call `flag.Parse()` itself. ## The shape ```go var ( port = flag.Int("port", 0, "port for the shared test listener; 0 picks a free one") dsn = flag.String("dsn", "", "database DSN for the suite; empty skips DB tests") ) func TestMain(m *testing.M) { flag.Parse() // *port and *dsn are now populated ... os.Exit(m.Run()) } ``` Calling `flag.Parse` twice is harmless in practice because `m.Run` checks whether parsing already happened, so the explicit call in TestMain simply moves it earlier. ## The classic bug: reading a flag at initialisation time ```go var dsn = flag.String("dsn", "", "database DSN") // Wrong: package variables are initialised long before any parse, // so this always builds a client from the empty default. var client = newClient(*dsn) ``` The pointer returned by `flag.String` is valid immediately - it points at the default - so nothing panics and nothing warns. The value simply never reflects the command line. The same mistake in an `init` function behaves identically. The rule is: register at initialisation, **read after parse**. ## Getting the flag past the go command `go test` interprets its own flags and forwards the ones it does not recognise to the test binary, so this works: ``` go test -v -port=9000 ./internal/transport ``` Two habits keep it unambiguous: - **Use `-name=value`, not `-name value`.** The go command has to decide where the package list starts, and an unfamiliar flag with a space-separated argument is exactly where that guess can go wrong. - **Or use `-args`.** Everything after `-args` is handed to the test binary untouched: `go test ./internal/transport -args -port=9000`. Because `-args` consumes the rest of the line, the package list must come first. ## Flags versus environment variables Both reach a test binary; they are not equivalent. - A **flag** is self-documenting (`go test -h` shows its usage string), is typed and validated by `flag`, and is visible in the command that failed. - An **environment variable** survives being passed through wrappers, `make` targets and CI job definitions that do not know about your flag, and it is the only option for something read by library code you do not control. A common split: a flag for the knob a developer turns by hand (`-port`, `-dsn`, `-update` for golden files), an environment variable for what the CI system injects. ## Design notes - **Give every flag a usable default**, so `go test ./...` with no arguments still does something sensible. A suite that only runs with a hand-written flag is a suite nobody runs. - **Keep the declarations in `_test.go` files.** A flag registered from non-test code appears in the real binary too, which is rarely intended. - **Watch the global namespace.** Flags register on the default `flag.CommandLine` set, so two packages in the same binary cannot both define `-port` - not a problem across packages, since each has its own test binary, but a real one if a helper package registers flags that a test package also wants. - **A flag that switches on an external dependency should degrade, not explode**: with an empty `-dsn`, skip the database tests rather than failing them, so the default `go test ./...` stays green on a laptop.

  • If TestMain does not call flag.Parse, does the flag still work in the tests?
    Yes. `m.Run` parses flags before running anything, so a test body reading `*port` sees the command-line value. Only code that runs earlier - TestMain's own setup, package variable initialisers, `init` functions - sees the default. That is exactly why TestMain has to parse explicitly.
  • When would you use an environment variable instead of a test flag?
    When the value is injected by a CI job, a Makefile or a wrapper script that does not know about your flag, or when it is consumed by library code you do not control. Flags are better for knobs a developer turns by hand, because `go test -h` documents them and they show up in the failing command.
  • Why should a custom flag have a working default?
    Because `go test ./...` with no arguments is what everyone actually runs - locally, in editors, in CI. A flag with a sane default keeps that path working; a flag that must be supplied turns the package into one nobody exercises. If the default cannot provide the dependency, skip those tests rather than failing them.

saying these in an interview costs you the question

  • Reads a flag value in a package variable or init
  • Assumes go test parses custom flags before TestMain
  • Registers the flag inside TestMain after flag.Parse
  • Prefixes a custom flag with -test. to get it forwarded
  • Requires a hand-written flag for the suite to run at all