go test compiles the package together with its _test.go files into a test binary and runs every func TestXxx(t *testing.T) in it; files and functions that do not follow those names are ignored.
**./...: every package below the current directory; without it, only the package you are in.
-run 'regexp'**: only tests whose name matches; -run 'TestParse/empty' picks one subtest, -run '^$' runs none (useful next to -bench).
**-v**: prints every test name and its t.Log output, not only the failures.
**-count=1**: forces a real run. With package arguments, successful results are cached, and an unchanged package prints ok example.com/store (cached). The cache only sees files inside the module and environment variables the test read, not a database or the network.
**-short, -race, -timeout**: testing.Short() lets slow tests skip; -race builds with the race detector; -timeout (10m by default) kills a hung binary and prints every goroutine's stack.
go test also runs a small, high-signal subset of go vet first, so a broken Printf format fails the run before any test does.
GoMiddle
Testing & Benchmarks
Table tests, httptest, benchmarks, fuzzing, -race in CI
Questions closed
0 / 25
- 0Review
- 4Asked often
Questions
A table-driven test lists its cases as a slice of structs (name, input, expected result) and runs one assertion over each; t.Run turns every case into a subtest with its own name and its own *testing.T.
The case name joins the parent's with a slash (TestParseDuration/no_unit), so a failure says which case broke.
go test -run 'TestParseDuration/no_unit' re-runs that one case; in a flat loop you would have to edit the source.
t.Fatal inside a subtest ends only that subtest, so one run reports every broken case. In a flat loop it ends the whole function.
A new case is one line of data, and a reviewer reads the inputs as a table rather than as copied code.
t.Error marks the test as failed and lets it continue; t.Fatal marks it failed and stops it immediately, so nothing after that line runs. t.Error is t.Log + t.Fail, t.Fatal is t.Log + t.FailNow.
Use t.Fatal when continuing makes no sense or would panic: Open returned an error, so the file handle is nil.
Use t.Error for independent checks: three wrong fields are reported in one run instead of three runs.
t.FailNow stops the test through runtime.Goexit, so deferred calls and t.Cleanup functions still run.
Call t.Fatal, t.FailNow and t.Skip only from the goroutine running the test: in a goroutine you started they end that goroutine, not the test.
t.Helper() makes a failure inside the helper point at the line that called it, not at the helper's own t.Fatalf; testing.TB is the interface that *testing.T, *testing.B and *testing.F all implement, so one helper serves tests, benchmarks and fuzz targets.
Without it every failure reads helpers_test.go:14, and you have to guess which of twenty callers broke.
Call it as the first statement of the helper, before anything can fail.
A closure inside the helper is a separate function: it needs its own t.Helper() call, or the reported line points inside the helper again.
package foo_test is an external test package: it sits in the same directory and ends up in the same test binary, but compiles separately, so it sees only foo's exported API and may import packages that themselves import foo.
Test as a real consumer: if a test needs an unexported field, the public API is probably missing something.
No import cycles: a foo/footest helper package that imports foo can be used from foo_test, but not from tests in package foo.
The cost: no access to unexported identifiers. The escape hatch is export_test.go: a file in package foo that re-exports what you need (var ParseKey = parseKey). Being a _test.go file, it never ships.
Mixing both in one directory is normal: internal tests for the algorithm, external tests for the behaviour.
An ExampleXxx() function in a _test.go file is documentation that runs: go test executes it, captures its standard output and compares it with the // Output: comment, and pkg.go.dev shows it next to Xxx.
The name ties it to an identifier: Example for the package, ExampleFormat for a function, ExampleWallet_Balance for a method, plus a lowercase suffix such as ExampleFormat_negative for several.
Leading and trailing whitespace is ignored. // Unordered output: compares the lines as a set, for map iteration.
Without an output comment the example is compiled but not run.
It has no *testing.T and checks only what it prints: good for showing the API, not for edge cases.
t.Cleanup(f) ties teardown to the test, not to the current function, so a helper can release what it opened instead of handing the caller a func() to remember.
A defer in a helper fires when the helper returns, too early for the database it just opened. t.Cleanup runs after the test and all its subtests finish, last registered first.
Parallel subtests start only after the parent's function has returned, so a defer in the parent has already closed what they need; t.Cleanup waits for them.
Go 1.24 added t.Context(), a context cancelled just before the cleanup functions run; it is a simple way to stop workers the test started.
defer is still right for anything scoped to the function you are in: a file opened in one loop iteration, a lock, a resp.Body.
func TestMain(m *testing.M) replaces the generated main of a package's test binary: it runs once per package, does the setup, calls m.Run() to run every test, then tears down.
Use it for process-wide state that is expensive or global: one Postgres container for the whole package, goleak.VerifyTestMain(m), a package logger. Per-test fixtures belong in helpers with t.Cleanup.
Flags are not parsed yet when TestMain starts: call flag.Parse() yourself if setup reads testing.Short() or a custom flag.
Since Go 1.15 simply returning is enough: the wrapper passes m.Run()'s result to os.Exit. Calling os.Exit(m.Run()) yourself skips your deferred teardown.
It runs once per package process, so go test ./... across 30 packages starts 30 containers, in parallel. A shared instance or -p keeps that under control.
t.Fatalf stops only the goroutine that calls it (here the worker, not the test) because it exits through runtime.Goexit. The wg.Done() after it never runs, so wg.Wait() blocks until -timeout kills the binary.
The testing docs allow FailNow, Fatal* and Skip* only on the goroutine running the test. Error* and Log* are safe from any goroutine.
defer wg.Done() would cure the hang, but the call would still break that contract.
If a worker outlives its test, a late t.Errorf panics with Log in goroutine after TestFetch has completed, which can take down the whole package.
go vet flags this: call to (*testing.T).Fatalf from a non-test goroutine.
The fix: workers return errors (over a channel or through errgroup), and the test goroutine makes the assertions.
Before Go 1.22 a range loop had one tc variable, reassigned on every iteration. t.Parallel() pauses each subtest until the parent's function returns; by then the loop has finished, and every closure reads the last case.
Go 1.22 creates a fresh variable per iteration, so each closure captures its own tc.
The rule follows the go line in go.mod, not the installed toolchain: upgrading Go without bumping that line keeps the old behaviour.
The old fix was tc := tc as the first line of the loop body; it is now redundant, and linters such as copyloopvar flag it.
The same pause means everything the parent defers runs before its parallel subtests, so use t.Cleanup for shared resources.
The subtest returns on any error without looking at tc.wantErr, so an unexpected error makes a valid case pass. When ParsePort starts rejecting every port, all three cases stay green.
Why: the return was meant for the error cases, but nothing ties it to them: the wantErr column is never read. The test checks only success values, and a function that fails on everything produces none to check.
The fix: compare the error with the case: (err != nil) != tc.wantErr fails on an unexpected error and on a missing one. t.Fatalf stops the subtest, because got means nothing after an unexpected error. For a specific error, put a sentinel in the table and check it with errors.Is.
Coverage is not a safety net: it counts executed statements, not checked ones. The test body runs every line and stays green; a drop in ParsePort coverage is only a side effect of this particular break, and a bug that returns a wrong error for valid input would keep coverage unchanged.
How to catch it: watch each new test fail once: break the code on purpose, or run a mutation tool such as gremlins, and check that some case goes red. A table column the test body never reads is a review red flag.
The recorder tests a handler, the test server tests a client: httptest.NewRecorder is a fake http.ResponseWriter you pass straight to ServeHTTP, while httptest.NewServer starts a real HTTP server on a loopback port.
Handler side: h.ServeHTTP(rec, httptest.NewRequest("POST", "/orders", body)), then assert on rec.Result(). No socket, no client, fast.
Client side: ts := httptest.NewServer(h) and point the code under test at ts.URL, so the base URL must be injectable. The handler can return a 503, hang or send broken JSON, which is the cheapest way to test retries and timeouts. ts.Client() already trusts a NewTLSServer certificate.
What the recorder skips: the network: no automatic Content-Length, no chunking, no HTTP/2, no timeouts, and it is not an http.Hijacker, so a WebSocket upgrade needs a real server.
Make the service depend on small interfaces declared where they are used, and pass a test double in the test: a hand-written fake in most cases, a generated mock (gomock, mockery) only when the exact calls are the behaviour under test.
Stub returns canned answers. Fake is a working lightweight implementation, such as an in-memory map store. Spy records calls to assert on later. Mock is programmed with expectations and fails the test if the calls differ.
In Go a fake is a few lines: a struct with a map, or a struct with function fields so each test overrides one method.
Generated mocks (go.uber.org/mock, which is the maintained fork of the archived golang/mock, or mockery with testify/mock) check call order and arguments. That welds tests to the implementation: a refactor that keeps the behaviour breaks them.
Fakes only repeat your assumptions about the other side. Check those separately: httptest.NewServer for the HTTP API, a real database in the integration lane for SQL.
testify is mostly assertions: require stops the test like t.Fatal, assert continues like t.Error.
Inject the clock: the code gets "now" from a dependency (a func() time.Time field or a one-method interface), production passes time.Now, and the test passes a fake it moves forward by hand. No sleeping, no flake.
time.Sleep in a test is a bet on how loaded the CI machine is: under load two seconds are not enough, and ten seconds make every run slow.
The smallest seam is a field, now func() time.Time, that defaults to time.Now when nil.
The same idea covers other non-determinism: the random source, the ID generator, the http.RoundTripper.
When the timing hides in goroutines and timers you cannot easily inject into (time.AfterFunc, a Ticker), Go 1.25's testing/synctest fakes the clock without changing the code.
Keep the expected output in testdata/ and rewrite it only under your own -update flag: a normal run compares against a committed file, and regenerating it shows up as a diff in code review.
Declare var update = flag.Bool("update", false, "rewrite golden files"). Under it, write the file and return; otherwise read and compare. Never both in one run.
The trap is rewriting on mismatch: a test that repairs itself can never fail. A missing golden file is a failure too, not a reason to create one.
Normalise timestamps, absolute paths and map-ordered output before comparing, or you have built a flaky test instead.
testdata is ignored by the go tool as a package, so it is the conventional home for fixtures.
Unit-test your logic with fakes; write integration tests for what your code only assumes about the other side: SQL, migrations, transactions, wire formats. Put them behind a //go:build integration tag so go test ./... does not compile them and CI runs them with -tags=integration.
Integration earns its cost where a fake would just repeat your assumptions: a query that is wrong only in real SQL, a unique constraint, an isolation level.
The directive must be above the package clause, with only blank lines and // comments before it; leave a blank line after it so it does not become the package doc. Below package, or written // go:build with a space, it is silently ignored and the file builds everywhere.
The lighter option is if testing.Short() { t.Skip("needs postgres") } with go test -short: one set of files, but the heavy dependencies compile anyway.
For the database itself, testcontainers-go starts a throwaway Postgres from the test; a CI service container works too. Give each test its own schema, or a transaction rolled back in t.Cleanup, so tests stay independent.
Coverage measures which statements the tests executed, not whether anything was checked. It can prove code is untested; it can never prove that the code which ran is correct.
A test with no assertions covers exactly the same lines as a good one: calling the function is enough.
Statements are not inputs: a one-line function with three boundary conditions is 100% covered by a single case.
Under a hard gate the last few per cent are error branches you would have to fake a disk failure to reach, so people write tests that assert nothing or delete the error handling. Many teams settle on a target around 70–80% plus review of what is uncovered.
How to see it: go test -coverprofile=cover.out ./..., then go tool cover -html=cover.out for the uncovered lines; -coverpkg=./... counts code exercised from other packages. Since Go 1.20, go build -cover measures a whole binary run by integration tests.
A benchmark is func BenchmarkXxx(b *testing.B) in a _test.go file, run with go test -bench=Xxx; the framework repeats the body until the run lasts -benchtime (1s by default) and reports the time per operation.
**b.N**: the classic for i := 0; i < b.N; i++ loop. The function is called several times with a growing b.N (1, then bigger estimates, capped at 1e9) until it fills -benchtime. Setup before the loop must be excluded with b.ResetTimer().
**b.Loop()** (Go 1.24): for b.Loop() { ... }. The function runs once per -count, only the loop is timed, so setup and teardown need no ResetTimer, and the compiler keeps the calls in the body from being optimised away.
Memory: b.ReportAllocs() or -benchmem add B/op and allocs/op, often the steadier signal.
Shape: -run '^$' skips the unit tests; b.Run gives sub-benchmarks per input size; -cpuprofile shows where the time goes.
A fuzz target states a property instead of an example: f.Fuzz runs your function on coverage-guided mutations of the seed inputs, so it reaches inputs nobody would think to write down.
The shape: func FuzzParseHeader(f *testing.F), seeds via f.Add(...), then exactly one f.Fuzz(func(t *testing.T, s string) { ... }). Arguments are limited to []byte, string, bool, rune, byte and the numeric types.
Without -fuzz it runs as an ordinary test over the seeds, in milliseconds. go test -fuzz=FuzzParseHeader -fuzztime=60s does the mutating and writes any failing input to testdata/fuzz/; commit that file and the plain run reproduces the bug forever.
It is worth it for code that eats untrusted input (parsers, decoders, protocol handlers) and when you can check something true of every input: Parse(Format(x)) == x, agreement with a reference implementation, or simply "never panics".
Check which goroutines are still alive when the test ends: go.uber.org/goleak does it with defer goleak.VerifyNone(t) in one test or goleak.VerifyTestMain(m) for the whole package, and fails with the stacks of the goroutines left behind.
Ordinary assertions cannot see a leak: the function returns the right value, and its worker stays blocked on a channel forever. In a server that is one goroutine and its memory per request.
goleak retries for a short while before failing, so goroutines that are already on their way out do not cause false failures.
Known long-lived goroutines, such as a library's background flusher, are excluded with goleak.IgnoreTopFunction(...) or goleak.IgnoreCurrent().
VerifyNone does not work with t.Parallel(): other tests' goroutines look like leaks. Use VerifyTestMain there.
Without a dependency you can compare runtime.NumGoroutine() before and after with a retry loop: cruder, and without stacks.
An empty loop. The compiler inlined scale, saw that the result is never used and deleted the body; 0.25 ns/op is about one CPU cycle, the cost of incrementing i.
The tells: a time near one cycle, b.N hitting the 1e9 cap for real work, or a number that does not move when the input doubles. go build -gcflags=-m shows can inline scale.
Fix before Go 1.24: make the result observable: accumulate it inside the loop and store the total in an exported package-level var Sink after the loop. Storing only the last result is not enough; the other iterations can still be dropped.
Constant inputs: scale(3, 3) can be folded to a constant even when the result is kept. Feed inputs from a slice or a variable.
Go 1.24+: for b.Loop() { scale(x, 3) }: the toolchain keeps the arguments and results of calls in the loop body alive, today by not inlining those calls. Use b.Loop or b.N, never both in one benchmark.
slices.Sort sorts data in place, so only the first iteration sees shuffled input; every later one sorts an already sorted slice, which pdqsort finishes in near-linear time. The 0.1 ms is the best case, about 100 times faster than the real sort.
Why: b.Loop calls the benchmark function once and repeats only the loop body, so setup above the loop runs once. The classic b.N loop has the same flaw within each call. Anything that mutates its input (a sort, an in-place dedupe, a reader consuming a buffer) changes the input of the next iteration.
The fix: keep the pristine data and copy it into a preallocated buf at the start of each iteration. The copy of 100,000 ints costs about 30 µs against about 10 ms of sorting, well under 1%; benchmark the copy alone if you need to subtract it.
**Not b.StopTimer around the copy**: b.StartTimer reads the runtime's memory statistics on every call, an overhead that distorts a short body more than the copy does.
How to catch it: a result that does not move when the fixture changes from random to sorted, or one far from a single timed call. Assert after the loop that the fixture is untouched, e.g. slices.IsSorted(data) is still false.
One run per side is one sample. Run each side about ten times into a file and let benchstat say whether 9% is larger than the machine's own noise.
Noise: CPU frequency scaling, thermal throttling, other processes and even code layout move the result by several per cent between two runs of the same binary.
Workflow: go test -run '^$' -bench=Encode -benchmem -count=10 > old.txt, make the change, the same command into new.txt, then benchstat old.txt new.txt. A ~ means the difference is not significant at p<0.05; that is an answer, not a broken tool.
Allocations: allocs/op is far steadier than ns/op. Four allocations down to one is real evidence even when the time column says ~.
Environment: a quiet machine on mains power, the same Go version on both sides, and the two runs close together in time; on shared CI runners only big differences mean anything.
synctest.Test (Go 1.25) runs the test function in a "bubble" with a fake clock: time inside moves only when every goroutine in the bubble is blocked, so a one-minute timeout passes instantly and deterministically, and synctest.Wait() waits until background goroutines settle.
How the bubble works
Goroutines started inside belong to it. time.Now, time.Sleep, timers, tickers and context.WithTimeout use the bubble's clock, which starts at midnight UTC on 2000-01-01.
The clock jumps to the next timer only when all goroutines are durably blocked: on channels created in the bubble, time.Sleep, select, sync.Cond.Wait, sync.WaitGroup.Wait (when Add ran in the bubble). A mutex, a syscall or real network I/O does not count; the bubble waits for it like real work.
synctest.Wait() returns when every other goroutine in the bubble is durably blocked: the deterministic replacement for "sleep a bit so the worker catches up".
Limits and extras
Code that waits on a real socket never lets the clock move; use an in-memory connection such as net.Pipe.
When the function returns, Test waits for all bubble goroutines to exit and panics on a deadlock: a free leak check.
Go 1.24 shipped it as an experiment (GOEXPERIMENT=synctest, synctest.Run); Go 1.25 made synctest.Test stable.
An injected clock is still the simpler choice for plain code that only reads the time.
Retries hide the bug; make the suite hostile instead. -race, -count=N and -shuffle=on turn the three usual causes (a data race, shared state and order dependence) into failures you can reproduce.
**-race** reports two goroutines touching the same memory without synchronisation, with both stacks. It only sees code that actually runs, so pair it with -count and real parallelism.
**-shuffle=on** randomises test order and prints the seed; -shuffle=<seed> replays it. A test that passes only after another one ran depends on global state.
**-count=50 -run TestX** on one suspect, ideally under CPU load (-cpu=1,4 or stress-style parallel runs), turns a 5% flake into a near-certain failure.
The usual culprits: a package-level singleton, a testdata file one test rewrites, os.Setenv instead of t.Setenv, a hard-coded port instead of port 0, a shared /tmp path instead of t.TempDir().
**time.Sleep as synchronisation**: the biggest single source of flakes. Wait on a channel with a deadline, or use testing/synctest.
Quarantine a known flaky test with t.Skip and a ticket rather than a retry, so the signal stays visible.