How does go test -run select a single t.Run subtest, and how are subtest names formed?
answer
- the name is a path, not a label
- spaces do not survive
- slashes split the pattern
- each element is a regexp, unanchored
- matching no case still exits zero
basics
~20 sA subtest's full name is the parent name, a slash, then the case name with spaces turned into underscores. go test -run splits its pattern on slashes and matches each part as an unanchored regexp.
solid answer
~40 s`t.Run("empty input", ...)` inside `TestParseExpr` produces the name `TestParseExpr/empty_input` — spaces become underscores, non-printable bytes are escaped, and a duplicate name gets a `#01`-style suffix. `go test -run` takes that hierarchy seriously: the pattern is split on unbracketed slashes and each element is an unanchored regexp matched against the matching level of the name. So `-run 'TestParseExpr/precedence'` runs only that row, and because matching is unanchored it would also match `precedence_unary`; anchor it with `-run 'TestParseExpr/^precedence$'` when you want exactly one. The parent must match too — the filter cannot reach a subtest whose parent is excluded. A pattern that matches the parent but no subtest is the trap: the parent runs, the loop selects nothing, and the package reports `ok`, so a typo looks like a green run.
code
text · 7 lines$ go test -v -run 'TestParseExpr/^precedence$'
=== RUN TestParseExpr
=== RUN TestParseExpr/precedence
--- PASS: TestParseExpr (0.00s)
--- PASS: TestParseExpr/precedence (0.00s)
PASS
ok example/parser 0.003sgo deeper
Know that a subtest is addressed as Parent/case_name with a slash, and that you can copy that name from a failure report straight onto a go test -run command line.
Explain the mechanics: slashes split the pattern, each element is an unanchored regexp, spaces in case names become underscores, and duplicates get a #01 suffix.
Point out the operational trap - a filter that matches nothing still exits zero - and argue that -run belongs in the local debug loop, never in the CI invocation.
Frame case names as a stable interface between CI output and the engineer debugging at 3am: names generated from data or timestamps break the copy-paste-and-reproduce loop for everyone.
## How the name is built Every test has a full name. A top-level `func TestParseExpr(t *testing.T)` is `TestParseExpr`. A subtest created by `t.Run(sub, f)` is the parent's full name, a `/`, and `sub` after the `testing` package rewrites it: - **Spaces become underscores.** `t.Run("empty input", ...)` inside `TestParseExpr` is `TestParseExpr/empty_input`. This exists so the name is one whitespace-free token in `go test -v` output and on a command line. - **Non-printable characters are escaped**, so a case name built from raw input bytes cannot corrupt the output stream. - **Duplicates are disambiguated.** Two rows named `utf8` become `TestParseExpr/utf8` and `TestParseExpr/utf8#01`. This is why an empty `name` field yields `#00`, `#01`, `#02` — useless in a failure report. - **Nesting composes.** A `t.Run` inside a `t.Run` gives a three-element name, `TestParseExpr/utf8/combining_mark`. Because the name is derived from your table's `name` column, the column is not decoration: it is the identifier you and CI will use to talk about the case. ## How -run matches it `go test -run <pattern>` filters which tests run. The pattern is **split on unbracketed slashes** into a sequence of regular expressions, and element *i* of the pattern is matched against element *i* of the test's name. Elements beyond the end of the pattern are unconstrained — they all run. ``` -run 'TestParseExpr' -> the whole test: every row runs -run 'TestParseExpr/precedence' -> only rows whose name contains "precedence" -run 'TestParseExpr/^precedence$' -> exactly the row named "precedence" -run '/precedence' -> any test's subtest matching precedence ``` Two properties trip people up: **Matching is unanchored.** Each element is a regexp searched inside the name, not compared for equality. `-run 'TestParseExpr/precedence'` also selects `precedence_unary` and `unary_precedence`, and `-run 'TestParse'` also selects `TestParseTemplate`. Anchor with `^...$` per element when you mean exactly one. **Every ancestor must match.** The runner cannot reach a subtest whose parent was filtered out, so `-run '/precedence'` still has to evaluate the parent element — an empty pattern element matches everything, which is why the leading-slash form works. A subtlety worth internalising: the filter is applied when `t.Run` is called, and the loop still runs. Your `range` iterates over all fifty rows either way; `t.Run` simply returns immediately for the rows that do not match. That is fine for a table of literals, and it is a reason to keep expensive per-case setup **inside** the subtest function rather than in the loop body — otherwise `-run` filters the assertion but not the cost. ## The silent-green trap Type `-run 'TestParseExpr/preced3nce'` and you get: ``` ok example/parser 0.002s ``` The parent matched, so `TestParseExpr` ran; no row matched, so nothing was checked; the package passed. Nothing warns you, because a test that runs zero subtests is not a failure. This is why `-run` belongs in your local debug loop and **never** in the command CI runs, and why `-v` is worth adding when filtering: `=== RUN` lines tell you at a glance whether the case you meant actually executed. ## Using it in practice The workflow this pattern is designed for: CI reports `--- FAIL: TestParseExpr/multibyte_identifier`, you copy that name verbatim onto your own command line, and run ``` go test -v -run 'TestParseExpr/^multibyte_identifier$' ./parser ``` to iterate on one case in milliseconds. That copy-paste only works if the name is stable and unique, which is the practical argument for descriptive `name` fields, for keeping names free of characters the shell will fight you over, and for not generating names from the input data itself (a name containing `/` splits into an extra level; a name derived from a timestamp is unreproducible). Related flags in the same family: `-count=1` defeats the test cache when you want a genuine re-run, and `-list` takes a regexp and prints matching top-level test names without running them — note it lists top-level tests only, since subtests do not exist until the parent runs.
- Why is -run 'TestParseExpr/precedence' not exactly one case?Each slash-separated element of the pattern is an unanchored regular expression, so it matches any subtest whose name contains that text - precedence, precedence_unary and unary_precedence all qualify. Anchor the element as ^precedence$ when you want a single row.
- You run go test with a -run pattern containing a typo in the case name and the package reports ok. What happened?The parent test still matched and ran; inside it, t.Run returned immediately for every row because none matched the second pattern element. Zero subtests ran, and running no subtests is not a failure. Add -v and look for the === RUN lines to confirm the case actually executed.
- What name does a table row get if the name field is left empty?The testing package falls back to a positional identifier, so the rows come out as TestParseExpr/#00, #01 and so on. They are unique but say nothing about the behaviour, which defeats the point of subtests in CI output and makes -run selection guesswork.
saying these in an interview costs you the question
- Thinks -run does exact string matching on names
- Expects a space in a case name to survive in the test name
- Believes a non-matching -run pattern fails the run
- Thinks the range loop stops iterating for filtered-out rows
- Assumes -run can reach a subtest without matching its parent