Last Updated: May 22, 2026
Go ships with a built-in test runner and a standard library package called testing, so you don't need a third-party framework to write or run tests. A test is just a Go function in a file ending with _test.go, and go test finds and runs every such function in a package. This chapter covers the conventions, signatures, command-line flags, and reporting methods that the rest of the testing chapters build on.
The testing package defines one type (*testing.T) and a few methods on it. The go test command is the runner: it compiles your package and any _test.go files into a temporary binary, runs the test functions, and reports results.
Here's the smallest useful test. Imagine a cart package with a Total function that adds up item prices.
The test lives next to it:
Run it from the cart directory:
go test looked at every file ending in _test.go and compiled them into the test binary. It then found every function whose name starts with Test and whose signature is func(t *testing.T), and ran each one. The PASS line means every test in the package passed; the ok line names the package and reports how long the binary took to run.
A failing test changes both lines:
The exit code matters. go test returns 0 on success and non-zero on failure, which is what continuous integration systems use to gate merges. You don't usually check $? by hand, but every CI tool you'll ever use does.
The temporary binary is built fresh each run. go test deletes it when the run ends, so you won't find a cart.test file lying around unless you ask for one with go test -c.
Files ending in _test.go are special. The Go compiler ignores them during a normal go build, so test code, mock objects, and test-only helpers never end up in production binaries. They're compiled and linked only when go test runs.
A typical package layout:
You can put multiple _test.go files in the same package. Convention is to mirror each source file with a matching test file (cart.go paired with cart_test.go), but that's a style choice, not a rule. Some teams group tests by feature instead. Either works.
Files that don't end in _test.go but live in the same directory get compiled into the package normally. If you want a helper that's only available to tests, put it in a _test.go file:
sampleCart is now visible to every test in the package but doesn't ship in the production binary. This is how you keep test fixtures, mocks, and fake clocks out of your release builds.
Every test function must follow three rules:
Test followed by an uppercase letter (TestTotal, TestApplyDiscount).func(t *testing.T)._test.go file.Break any one of those and go test either silently ignores the function or fails to compile.
The uppercase letter after Test matters because of Go's naming rules. Testtotal (lowercase second letter) is treated as a single lowercase identifier and skipped. TestTotal is a valid test name. So is Test_Total, though most Go codebases drop the underscore.
The *testing.T parameter is the bridge to the test runner. You call methods on it to report results, log messages, skip the test, or abort early. The runner watches what you call and uses that to decide whether the test passed.
A test that doesn't call any failure method on t is considered passing. You don't return a value, you don't raise an error. Silence equals success.
go test accepts package paths, just like go build and go run. Without arguments, it tests the package in the current directory.
Inside each package, the runner builds the test binary, then scans the binary's exported symbol table for functions matching TestXxx, BenchmarkXxx, ExampleXxx, and FuzzXxx. This chapter focuses on TestXxx.
Tests within a single package run sequentially in the order they appear in the source files, in the order Go's loader presents those files (alphabetical by filename). You shouldn't depend on the order, because anyone can rename a file or reorder declarations and your suite still has to pass. Tests in different packages can run in parallel, which speeds up go test ./... on large modules.
If any package's tests fail, the whole go test ./... invocation exits non-zero. The summary at the end of the run lists which packages passed and which failed, so you can see the damage at a glance even when the output is long.
Cost: go test ./... compiles every test binary in the module before running anything. On a large monorepo that's noticeable. go test ./pkg/cart/... scopes the work to one subtree when you're iterating on a single package.
Four flags cover most day-to-day work. There are more, but these handle the iteration loop.
`-v` (verbose) prints every test name as it runs, plus any t.Log output. By default, go test swallows successful output and shows only failures, which is great for CI logs and noisy on a laptop when you're debugging.
`-run` filters which tests run by regular expression. The pattern is matched against the test name, so -run Total runs every test whose name contains Total. Anchors work: -run '^TestTotal$' runs only the exact name.
This is the flag you'll use most while debugging a single broken test. Running the whole suite each time wastes seconds, and seconds add up.
`-count=N` runs each test N times instead of the default once. Useful for catching flaky tests that pass intermittently. Go's compiler also caches test results when the inputs haven't changed; -count=1 is the idiom for forcing a fresh run after touching environment variables or external state.
Cost: -count=10 re-runs every test (including its setup, including any TestMain work) ten times. If a test opens a database connection or seeds large fixtures, the wall-clock cost multiplies. Use it deliberately, not as a default.
`-failfast` stops the run as soon as the first test fails, instead of continuing through the rest. Useful when you've broken something fundamental and the cascade of follow-on failures is noise.
These flags compose. A common debug invocation is go test -v -run TestTotal -count=1, which runs only the named test, prints everything, and skips the cache.
The *testing.T type has five reporting methods. They split into two pairs (error vs fatal) and a logger.
| Method | Marks test as failed? | Stops the test? | Format string? |
|---|---|---|---|
t.Log | No | No | No |
t.Logf | No | No | Yes |
t.Error | Yes | No | No |
t.Errorf | Yes | No | Yes |
t.Fatal | Yes | Yes | No |
t.Fatalf | Yes | Yes | Yes |
The split between Error and Fatal is the important one. Error marks the test as failed but keeps running, so you can collect multiple assertion failures from one test. Fatal marks it failed and calls runtime.Goexit on the test goroutine, which stops the rest of the function immediately. Use Fatal when continuing would crash or produce garbage results.
The first check uses Fatalf because if New returned an error, o is nil and the next line crashes with a nil dereference. The two field checks use Errorf because each is independent, and you'd rather see both failures than have the first one hide the second.
The f suffix follows the same pattern as the fmt package: t.Error accepts a list of arguments and prints them with spaces between, t.Errorf takes a format string with verbs like %v, %q, %d. Prefer the f versions when you're describing what went wrong, because they let you embed values clearly.
The got = %v, want %v shape is the convention in the standard library and most Go codebases. It's worth adopting verbatim so your test output reads the same way as everyone else's.
t.Log and t.Logf write to the test's output buffer. By default that buffer prints only if the test fails or you passed -v. This is useful for "this is what the inputs looked like" diagnostics: include them in every test, and they're invisible until something breaks.
A passing run still hides the t.Logf line. A failing run shows it, giving you the exact inputs that triggered the failure.
Sometimes a package needs work done before any test runs: opening a database connection, starting a fake server, seeding a fixture file. TestMain is the hook for that. If you declare a function with this exact signature, go test calls it instead of running tests directly, and you decide when to run them.
Three rules to keep TestMain correct:
m.Run() exactly once. That's what actually runs your TestXxx functions.m.Run() and pass it to os.Exit. The runner reads the exit code to decide pass/fail.os.Exit, because os.Exit doesn't run deferred functions. Put cleanup before the os.Exit call, or use a wrapper pattern.A common mistake is forgetting os.Exit entirely. The tests still run, but the exit code is whatever the operating system uses for a normal return, which means failing tests get reported as passing in CI. The pattern in the snippet above is the safe form.
You only need TestMain when you actually have package-wide setup. For most packages, the implicit TestMain (which just calls m.Run() and exits) is fine, and you don't write the function at all.
_test.go files can live in one of two packages, and the choice changes what they can access.
Internal tests sit in the same package as the code under test. They use package cart (the same package name as cart.go). They can see unexported identifiers, which means they can test private functions, private types, and internal state directly.
External tests sit in a separate package named cart_test (with the _test suffix). They can only see exported identifiers, which forces them to test the package through its public API.
The cart_test package is a special case the Go toolchain allows: two packages with related names compiled into the same test binary. Files in package cart and files in package cart_test can both live in the cart/ directory at the same time.
Why have both? Internal tests are handy for checking the parts of your implementation that no caller should see, like the cents-rounding function above. External tests force you to use the package the way every other caller does, which catches API mistakes (a type that's hard to construct, an error that's hard to inspect, a missing accessor). Many Go codebases use external tests by default and switch to internal tests only when there's no public way to exercise a particular behavior.
There's also a subtle reason to prefer external tests: they avoid import cycles. If cart_test needs to import a package that itself imports cart, an internal test in package cart couldn't do that without a cycle. The external cart_test package is allowed because it's not part of cart's import graph.
A test binary exits with code 0 if every test passed and a non-zero code if any test failed. The default failure code is 1, though TestMain can override it by passing a different value to os.Exit. This is the only signal CI systems read; the PASS / FAIL text is for humans.
This is why a missing os.Exit(code) in TestMain is a silent disaster: the binary returns 0 even though tests failed, and CI marks the build as green. Always pass through the value from m.Run().
A panic inside a test also fails the test, with a stack trace included in the output. The exit code is still non-zero. The test binary catches the panic at the goroutine boundary, so a panic in one test doesn't kill the others. Other tests in the package still run.