{
  "$type": "site.standard.document",
  "canonicalUrl": "https://rednafi.com/go/test-config-with-flags/",
  "description": "Control Go test behavior with custom flags instead of build tags or env vars. Enable integration and snapshot tests with discoverable CLI options.",
  "path": "/go/test-config-with-flags/",
  "publishedAt": "2025-06-28T00:00:00.000Z",
  "site": "at://did:plc:fgtm2c26vfcj74rfmeggbyqj/site.standard.publication/3mnl6f7ob462z",
  "tags": [
    "Go",
    "Testing",
    "CLI"
  ],
  "textContent": "As your test suite grows, you need ways to toggle certain kinds of tests on or off. Maybe\nyou want to enable [snapshot tests], skip long-running [integration tests], or switch\nbetween real services and mocks. In every case, you're really saying, \"Run this test only if\n_X_ is true.\"\n\nSo where does _X_ come from?\n\nI like to rely on Go's standard tooling so that integration and snapshot tests can live\nright beside ordinary unit tests. Because I usually run these heavier tests in\n[testcontainers], I don't always want them running while I'm iterating on a feature or\nchasing a bug. So I need to enable them in an optional manner.\n\nTo fetch the _X_ and conditionally run some tests, you'll typically see three approaches:\n\n1. Build tags – place integration or snapshot tests in files guarded by build tags, so\n   they're compiled only when you include the tag.\n2. Environment variables – have each test look for an environment variable (e.g.,\n   RUN_INTEGRATION=1) and skip itself if it's absent.\n3. Custom go test flags (my preferred approach) – define your own flags so you can\n   run, for example, go test -run Integration -integration.\n\nBuild tags are hard to discover\n\nBuild tags are special comments you place at the top of a .go file to tell Go to include\nthat file only when certain tags are set during the build. This is how they typically look:\n\nThis file will only be compiled and included when you run:\n\nIf you don't pass the tag, the file is skipped entirely during the build. Go won't even see\nthe test.\n\nThe upside is that it gives you a clean separation. You can group slow tests or\nenvironment-dependent tests into their own files. But the downsides add up quickly.\n\nFirst, there's no way to discover which tags are used without grepping through the codebase.\nGo itself won't tell you. go help test doesn't mention them. There's no built-in list or\nsummary. You need to solely depend on documentation.\n\nSecond, build tags are applied per file, not per package. That means if even one test in a\nfile is guarded by a tag, the entire file is excluded unless the tag is passed. This makes\nit difficult to mix optional and always-on tests in the same file.\n\nAnd third, once you have more than a couple of tags, managing them becomes guesswork. You\nend up running things like:\n\nBut you no longer remember what each one does or what combinations are safe. There's no\nvalidation. It gets messy fast.\n\nEnvvars are a bit better\n\nEnvironment variables let you control test behavior at runtime. You don't need to recompile\nanything, and you can pass them inline when running tests.\n\nHere's a typical example:\n\nYou run it like:\n\nThis is more dynamic than build tags. You don't have to split tests into separate files, and\nyou don't have to rebuild with special flags. More importantly, the test itself can detect\nwhen the environment variable is missing and tell you what to do. It can skip itself and\nprint a message like \"set SNAPSHOT=1 to run this test.\" That feedback loop is helpful.\n\nBut the discovery problem remains. There's no built-in way to ask, \"what environment\nvariables does this test suite support?\" You still have to read the code to find out.\n\nIt can get worse if the check is buried deep in a helper. Maybe some setup logic does:\n\nNow the test runs, but the behavior changes silently based on the environment. Nothing in\nthe test output tells you that the envvar was involved. You may not even realize that you're\nrunning in a different mode.\n\nAnd just like with build tags, there's no central registry. No docs or summary. You can only\nhope someone left a good comment or wrote it down somewhere.\n\nCustom flags are almost always better\n\nThe cleanest and most discoverable way to control optional test behavior in Go is by\ndefining your own test flags. They're typed, explicit, and work well with Go's built-in\ntooling. Instead of toggling tests with magic file-level build tags or invisible environment\nvariables, you can wire up test configuration using the flag package, just like any other\nGo binary.\n\nThere are two common approaches for defining test flags:\n\n- Package-level flags via TestMain\n- Per-file flags via init().\n\nBoth approaches register the flag in the global flag set, so every test in the package can\nsee the value once parsing has happened. The trade-off is indirection versus locality:\nTestMain centralizes all flags in one place, while file-level init() keeps each flag\nnext to the code that cares about it.\n\nHere's how it looks with TestMain:\n\nAnd here's the equivalent using init() to keep everything in the same file:\n\nOnce you've defined a flag, you run the snapshot tests like this:\n\nYou can also list all the flags using:\n\nThis prints all registered flags, including your own:\n\nA detail about names: built-in flags show up in the help output with a test. prefix\n(-test.v, -test.run, -test.timeout), yet you pass them without that prefix (-v,\n-run, -timeout) while running tests. The Go tool strips test. for you. Custom flags\ndon't get this treatment. Whatever string you register is the exact string you must pass. If\nyou register snapshot you run:\n\nIf you register test.snapshot you must run:\n\nThere is no automatic collapsing just because the name starts with test..\n\nThe flag -args lets you pass additional arguments to the test binary. When the binary sees\n-h after -args, it prints every flag and exits. No tests run, though the binary is\nbuilt. That one command exposes the full configuration surface of your tests.\n\nIf you namespace your flags like this:\n\nThen you can grep for them:\n\nDefine the global flags in TestMain when several files need the same switches or when you\nhave package-wide setup (containers, databases, global mocks). Define flags in init() when\na switch is relevant to one test file and you want the declaration right next to the logic\nit controls. I usually prefer per-test- file-level flags that don't need to depend on any\nglobal magic.\n\nEither way, the flag lives in code, is easy to grep, appears in -h, and tells everyone\nexactly what it controls. The only downside I can think of with this approach is that,\nsimilar to the environment variable technique, you'll have to check for the flag in every\ntest and make a decision. But in practice, I prefer the flexibility over the all-or-nothing\napproach with build tags.\n\n---\n\nI think flags are the best way to configure your apps and tools. Even when environment\nvariables are involved, I often map them to flags for documentation purposes. The goal is to\ngive users a single -h command they can run to see all available options for tuning\nbehavior. Tests are no exception. I was quite happy to find out that Peter Bourgon conveyed\nthe same sentiment in this [seminal 2018 blog post].\n\n\n\n\n\n[snapshot tests]:\n    https://www.reddit.com/r/golang/comments/yytw1f/snapshot_testing_in_golang/\n\n[integration tests]:\n    https://www.reddit.com/r/golang/comments/18xmkuz/how_do_you_write_integration_tests_in_go/\n\n[testcontainers]:\n    https://testcontainers.com/\n\n\n[seminal 2018 blog post]:\n    https://peter.bourgon.org/go-for-industrial-programming/#program-configuration",
  "title": "Flags for discoverable test config in Go"
}