JavaRush /Courses /Go SELF /go test: -run, -count, -v, -failfast, and the cache

go test: -run, -count, -v, -failfast, and the cache

Go SELF
Level 31, Lesson 0
Available

1. What go test does and why flags matter

When you’ve just started writing tests, life seems simple: run go test, get a green “PASS”, go make some tea. But once you have more than ten tests and a failure happens “somewhere in the middle”, the typical student meditation begins: “why did it fail... and where exactly... and why again...”.

go test flags are your way to control execution: run not everything, but exactly what you need; see more details; stop a run earlier; and also understand why tests sometimes “finish instantly” (spoiler: it might be the cache). The official description of go test explicitly says that it compiles the package and *_test.go, then runs a separate test binary for the package.

Two execution modes: go test and go test . are not the same

Surprise number one (and it’s useful): go test works in two modes.

In “local directory mode” you run go test without package arguments (for example, just go test or go test -v) — and in this mode caching is disabled.

In the second mode, “package list mode”, you explicitly specify package(s): go test ., go test ./..., go test math. And here go test can cache successful results so it doesn’t rerun the same work again and again.

To make this feel less like magic, here’s a diagram:

flowchart TD
    A["You run go test"] --> B{"Are packages specified? (go test . / ./...)"}
    B -- "no (go test)" --> C["Local directory mode: no cache"]
    B -- "yes" --> D["Package list mode: cache possible"]
    C --> E["Build test binary Run Show result"]
    D --> F{"Is there a suitable cached result?"}
    F -- "yes" --> G["Show previous output instead of running (you'll see '(cached)')"]
    F -- "no" --> E

2. The -v flag: verbose output

When tests fail, a beginner usually wants two things: to understand which test failed, and to see the context. By default, go test often saves output and shows many details only when the package fails. But there’s a mode where it becomes talkative: -v.

The go test help says that -v enables verbose output: it logs tests as they run and prints text from Log/Logf, even if the test succeeds. It’s also important that in package list mode go test prints full output even for successful packages if you specified -v (or -bench).

Mini example: a small function and tests

Imagine that in a learning project you have a todo package, and you want to normalize task titles (so you don’t store " buy bread " as-is).

package todo

import "strings"

// NormalizeTitle makes the title look neat.
func NormalizeTitle(s string) string {
	return strings.TrimSpace(s)
}

A table-driven test with subtests:

package todo

import "testing"

func TestNormalizeTitle_Table(t *testing.T) {
	cases := []struct {
		name, in, want string
	}{
		{name: "trim", in: "  buy milk  ", want: "buy milk"},
		{name: "empty", in: "   ", want: ""},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			if got := NormalizeTitle(tc.in); got != tc.want {
				t.Fatalf("NormalizeTitle(%q)=%q, want %q", tc.in, got, tc.want)
			}
		})
	}
}

Now the command:

go test -v ./...

In verbose mode you’ll see which tests actually started, including subtests. This is especially useful when you have many tests and want to quickly confirm that you’re running exactly what you expect.

3. The -run flag: run only the tests you need

Sometimes you don’t want to “run everything”, but to quickly check one specific thing. For example, you’re fixing one test and don’t need to wait while 300 others run. That’s what -run is for.

The official description says: -run regexp runs only those tests/examples/fuzz tests that match the regular expression.

How -run works with subtests (t.Run)

The most useful part is that -run understands subtest names. The docs explicitly say: for tests, the regexp is split on the / character into a sequence of regular expressions, and each part of the test identifier must match its corresponding regexp.

So the test TestNormalizeTitle_Table/trim can be targeted like this:

go test -v -run TestNormalizeTitle_Table/trim ./...

And here’s an important nuance: the docs say that possible parents of matches are also run, so -run=X/Y may run all tests matching X, even if they have no subtests matching Y — because they must be executed in order to even “look for” those subtests.

That is, -run TestNormalizeTitle_Table/trim guarantees that TestNormalizeTitle_Table starts, and then inside it the system filters the needed subtests.

4. The -failfast flag: failed — don’t start new tests

When you have many tests, one failure often triggers an avalanche: one test breaks the state and then the rest fail; or you simply have many independent failures but you need to see the first one to start there.

-failfast does exactly this: “Do not start new tests after the first test failure.” Note the wording: it doesn’t promise to instantly stop what’s already running; it says “do not start new”. For a beginner this is the right mental model: once something fails, the run “tries to wrap up neatly” and doesn’t fan out into even more tests.

Mini example

Imagine you temporarily broke an expectation:

t.Run("trim", func(t *testing.T) {
	if got := NormalizeTitle("  buy milk  "); got != "BUY MILK" {
		t.Fatalf("unexpected result: %q", got)
	}
})

If you run it like this:

go test -failfast -v ./...

then after the first failure it will not continue starting new tests (where possible).

5. Test caching: why sometimes “everything finished in 0.00s”

Now for the most mysterious part: you run go test ./..., and it finishes quickly. Then you run it a second time — and it finishes suspiciously fast. And you start suspecting either a conspiracy or that Go is “too smart”.

In reality, go test really does cache successful results, but only in package list mode. The docs say: “In package list mode only, go test caches successful package test results…”, and if the result is taken from the cache, go test replays the previous output instead of rerunning the binary and prints (cached) instead of the time.

What must match for the cache to work

The cache has rules. The docs say: the same test binary must match, and the flags must come from a limited set of “cacheable flags”. The list includes, among others, -failfast, -run, and -v.

This is a good point to understand: you can target specific tests with -run and still get caching, as long as the rest of the command is “cacheable”.

Let’s make a small table so it’s easier to keep in mind:

Flag What it’s about Affects caching?
-v
verbose output yes, it’s in the cacheable flags
-run
select tests by regexp yes, it’s in the cacheable flags
-failfast
don’t start new tests after the first failure yes, it’s in the cacheable flags
-count
how many times to run not in the cacheable flags: any value disables caching; the idiomatic form is -count=1

(This table is here for “quick recall”, not for memorizing word-for-word.)

6. The -count flag: reruns and an honest restart without cache

When you hear the word “count”, you might think “how many tests”. In reality it’s “how many times to run”. The docs say: -count n runs each test (and benchmark, and fuzz seed) n times; the default value is 1. There’s also an important detail: examples always run once, and -count does not apply to fuzz tests selected via -fuzz.

Why -count=1 is needed at all if “1” is already the default

Because -count=1 is used as an idiomatic way to explicitly disable test caching. This isn’t a guess — it’s stated directly in the cache description: “The idiomatic way to disable test caching explicitly is to use -count=1.”

Yes, it sounds strange: “count=1” seems like it “changes nothing”, but at the execution-mechanics level it forces go test to run the test binary again instead of using a previous cached result.

This is especially important in situations where a test depends on the outside world: time, environment, files, a database, the network (although unit tests shouldn’t touch the network, but life is full of surprises). And even if you’re sure your tests are “pure”, -count=1 is a good way to quickly answer: “Did it really run now, or did it just show the previous output?”

Mini debugging mode: my favorite combo

When you’re fixing one test, a typical combination looks like this:

go test -v -run TestNormalizeTitle_Table/trim -count=1 ./...

Here -run targets a specific subtest, -v provides detailed output, and -count=1 guarantees you’re seeing the result of this run, not “replayed” output from the cache.

A mini strategy for reading output so you don’t drown

When tests fail, it’s very tempting to immediately start poking at the code. But most of the time it’s faster to first make sure you are running what you need and are seeing what you need.

I usually do this mentally: first I enable -v to see the list of runs. Then, if the failure is localized, I add -run so I don’t run extra stuff. And if the result is “too fast and suspicious” — I add -count=1 to remove caching.

If “everything is failing”, sometimes it’s worth enabling -failfast so you start with the first cause and don’t look at 50 secondary symptoms.

7. Common mistakes

Mistake #1: thinking caching works “always”, and not understanding why sometimes it’s there and sometimes it isn’t.
A very common confusion comes from the two modes of go test. If you run go test without package arguments, that’s local directory mode, and caching is disabled there. But if you run go test . or go test ./..., that’s package list mode, and caching is possible there. If you don’t distinguish these modes, the behavior feels random — even though it’s fairly strict.

Mistake #2: using -run but not enabling -v, and then guessing “did it even run what I asked for?”.
-run filters tests, but without verbose output it’s hard to quickly see which TestXxx/subtest actually started. In verbose mode go test logs tests as they run. That’s why for diagnostics -run almost always pairs well with -v.

Mistake #3: being surprised that -run X/Y runs more than one subtest.
In go test, the regexp for tests is split on /, and the “parent” tests are still run in order to find subtests. That’s why -run=X/Y may run all tests that matched X, even if some of them have no subtests matching Y. It’s not a bug — it’s the logic of searching through the test tree.

Mistake #4: not distinguishing “stop everything” from “don’t start new” in -failfast.
-failfast doesn’t sound like a “kill switch”. It says: after the first failure, do not start new tests. If you already have some tests running (or package runs are already in progress), you may still see some output after the first error. That’s normal.

Mistake #5: “I fixed the bug, but the result is the same” — and forgetting -count=1.
If you run tests in package list mode, successful results can be taken from the cache, and you’ll see (cached) instead of the time. At the “fixed/not fixed” moment it’s very useful to explicitly add -count=1, because that’s the idiomatic way to disable caching.

1
Task
Go SELF, level 31, lesson 0
Locked
Test logs
Test logs
1
Task
Go SELF, level 31, lesson 0
Locked
One subtest
One subtest
1
Task
Go SELF, level 31, lesson 0
Locked
Run flags
Run flags
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION