Testing
Part of the toolchain, not a library you pick.
- Write a table-driven test with named subtests and clear failure messages.
- Write a fuzz test and a
b.Loopbenchmark, and run them withgo test. - Explain the difference between
t.Errorandt.Fatal.
Testing is part of the toolchain
A test file ends in _test.go and sits in the same folder as the code it tests. Each test is a function starting with Test that takes a *testing.T. That is the whole setup: no framework to choose, no config file, no annotations.
You run them with go test. It compiles the package plus its tests and reports what failed.
The Go convention: table-driven tests
Rather than one function per case, you put the cases in a slice and loop over them. Adding the twelfth case becomes one line, and t.Run gives each case a name you can run on its own.
package calcimport "testing"func TestAdd(t *testing.T) { tests := []struct { name string a, b, want int }{ {"positive", 2, 3, 5}, {"zero", 0, 0, 0}, {"negative", -1, -1, -2}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := Add(tt.a, tt.b); got != tt.want { t.Errorf("Add(%d, %d) = %d, want %d", tt.a, tt.b, got, tt.want) } }) }}Running them
go test ./... # everythinggo test -v ./... # name every test as it runsgo test -cover ./... # how much of the package is exercisedgo test -run 'TestAdd/negative' ./calc # exactly one subtestgo test -race ./... # find data races — run this in CIFuzzing: let Go invent the inputs
A table test only checks the cases you thought of. A fuzz test makes up thousands of inputs for you and looks for one that breaks a rule you state. It is a function starting Fuzz that takes a *testing.F. f.Add gives it a few starting inputs (the seed corpus), and f.Fuzz holds the check that must hold for every input.
package calcimport "testing"func FuzzReverse(f *testing.F) { f.Add("hello") // seed inputs: Go starts from these and mutates them f.Add("héllo") f.Fuzz(func(t *testing.T, s string) { // the rule: reversing twice gives back what you started with if got := Reverse(Reverse(s)); got != s { t.Errorf("Reverse(Reverse(%q)) = %q, want %q", s, got, s) } })}Benchmarks with b.Loop
A benchmark measures how fast code runs. It is a function starting Benchmark that takes a *testing.B. Since Go 1.24 you write the timed part inside for b.Loop() { ... }. Older code uses for i := 0; i < b.N; i++, which still works, but b.Loop is better: setup written before the loop is not timed, and the compiler cannot optimise away the call you are measuring.
func BenchmarkAdd(b *testing.B) { nums := []int{1, 2, 3} // setup: not timed for b.Loop() { Add(nums[0], nums[1]) }}Error versus Fatal
t.Errorrecords the failure and keeps going. Use it when the rest of the checks still make sense.t.Fatalrecords it and stops that subtest. Use it when continuing would panic, like after a failed setup.t.Helper()inside a helper function makes failures point at the caller, not at the helper.t.Cleanup(fn)runs teardown even when the test fails, and works from inside helpers wheredeferwould not.
- A
_test.gofile next to the code, aTestXxx(t *testing.T)function, andgo test. - Table-driven tests are the Go standard: cases as data, one loop,
t.Runfor subtests. go test -run "TestX/case_name"runs one case. The pattern is a regular expression.t.Errorcontinues;t.Fatalstops that subtest.
- 1Add a failing case and read how the failure is reported.
- 2Run
go test -v -cover ./...and read the coverage number. - 3Move your test into
package foo_testand see what breaks.