Testing & benchmarks
No framework. No external runner. go test finds every
_test.go file under the current package and runs anything matching
TestXxx / BenchmarkXxx / ExampleXxx.
Table-driven tests are the idiom. The race detector is one flag away.
1 · Try it — the simplest test
package math
func Add(a, b int) int { return a + b }package math
import "testing"
func TestAdd(t *testing.T) {
got := Add(2, 3)
want := 5
if got != want {
t.Errorf("Add(2,3) = %d; want %d", got, want)
}
}$ go test
PASS
ok example.com/math 0.003s
$ go test -v # verbose
=== RUN TestAdd
--- PASS: TestAdd (0.00s)
PASS2 · Table-driven tests — the idiom
The canonical Go test pattern: declare a slice of test cases, loop. Each case
gets a subtest via t.Run, which means individual cases can fail
independently and you can go test -run TestAdd/case-3 to narrow.
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
want int
}{
{"two positives", 2, 3, 5},
{"zero", 0, 0, 0},
{"negatives", -1, -2, -3},
{"mixed", 5, -3, 2},
{"overflow", 1<<62, 1<<62, -(1 << 63)}, // 2^62 + 2^62 wraps to math.MinInt64
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Add(tt.a, tt.b)
if got != tt.want {
t.Errorf("Add(%d, %d) = %d; want %d", tt.a, tt.b, got, tt.want)
}
})
}
}3 · t.Helper, t.Cleanup, t.Skip, t.Parallel
func assertEqual(t *testing.T, got, want any) {
t.Helper() // makes errors point at the caller, not this line
if got != want {
t.Errorf("got %v, want %v", got, want)
}
}
func TestThing(t *testing.T) {
if testing.Short() {
t.Skip("skipping in short mode")
}
t.Parallel() // run in parallel with other Parallel tests
f, err := os.CreateTemp("", "test")
if err != nil { t.Fatal(err) }
t.Cleanup(func() { // runs on test end (success or failure)
f.Close()
os.Remove(f.Name())
})
// ... test body ...
assertEqual(t, 1+1, 2)
}Fatal stops the test
immediately. Error records a failure but keeps going. Use
Fatal when continuing makes no sense (setup failed). Use
Error for assertions where you'd like to see all failures.4 · Benchmarks
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(2, 3)
}
}
// Subtests apply to benchmarks too — useful for parameter sweeps
func BenchmarkSort(b *testing.B) {
for _, size := range []int{10, 100, 1000, 10000} {
b.Run(fmt.Sprintf("n=%d", size), func(b *testing.B) {
data := makeData(size)
b.ResetTimer() // exclude setup
for i := 0; i < b.N; i++ {
sort.Ints(append([]int{}, data...))
}
})
}
}$ go test -bench=. -benchmem
goos: linux
goarch: amd64
BenchmarkAdd-8 1000000000 0.301 ns/op 0 B/op 0 allocs/op
BenchmarkSort/n=10-8 18432651 65.4 ns/op 8 B/op 1 allocs/op
BenchmarkSort/n=100-8 1234567 972 ns/op 8 B/op 1 allocs/op
BenchmarkSort/n=1000-8 87654 13520 ns/op 8 B/op 1 allocs/op
BenchmarkSort/n=10000-8 9123 131234 ns/op 8 B/op 1 allocs/op5 · The race detector
# Run all tests with the race detector
$ go test -race ./...
# 2-10x slower but catches every data race the test triggers.
# CI should run with -race on every PR.If two goroutines access the same memory without synchronization and at least one writes, the race detector fires. It instruments memory access at compile time. A test that doesn't fail but reports "DATA RACE" is still a failure — your code has a bug.
6 · Examples — tests that are documentation
func ExampleAdd() {
fmt.Println(Add(2, 3))
// Output: 5
}
// Multi-line output
func ExampleSum() {
for _, v := range Sum([]int{1, 2, 3}) {
fmt.Println(v)
}
// Output:
// 1
// 3
// 6
}Functions named ExampleXxx are tested AND shown as documentation in
go doc and pkg.go.dev. The // Output: comment is
matched against actual output — example breaks if output changes. Free
documentation that can't drift.
7 · Fuzz testing (Go 1.18+)
func FuzzReverse(f *testing.F) {
// Seed corpus
f.Add("hello")
f.Add("")
f.Add("a")
f.Fuzz(func(t *testing.T, in string) {
out := Reverse(Reverse(in))
if in != out {
t.Errorf("Reverse(Reverse(%q)) = %q; want %q", in, out, in)
}
if !utf8.ValidString(out) {
t.Errorf("invalid UTF-8: %q", out)
}
})
}
// $ go test -fuzz=FuzzReverse -fuzztime=30s
// The runtime mutates the seed corpus, generates new inputs, looks for
// crashes. Catches things you'd never think to test by hand.8 · Common mistakes
- Reaching for testify/Ginkgo/Gomega. The standard library has everything.
if got != want { t.Errorf(...) }is enough. Reach for libraries only when reflection-based assertions actually pay off. - Forgetting
t.Helper(). Without it, assertion errors point at your helper, not the test that called it. - Tests that share state. Two tests modifying a global. Make each test set up its own state via
t.TempDir(),t.Cleanup. - Tests that depend on order.
go testmay run subtests in parallel. Don't depend on side effects between tests. - Slow tests in the default set. Use
testing.Short()+-shortflag for the slow integration suite. - No benchmark baseline. A bench result alone is meaningless. Run before and after the change; compare with
benchstat.
9 · Exercises (~15 min)
- Table-driven for a Reverse function. 5 cases including empty, single char, palindrome, mixed.
- Benchmark slice vs array. Compare slicing a 1000-element array vs allocating a slice.
go test -bench=. -benchmem. - Run with -race. Spawn two goroutines writing to a shared int. Run the test under
-race. Watch it fire. - Add an Example. Write
ExampleAddfor your function with// Output:comment. Rungo test— the example runs and validates.
10 · When it clicks
- You write tests in the same commit as the code, not after.
- You reach for table-driven structure by reflex — even for 3 cases.
- Your CI runs
go test -race ./...on every PR. - You write benchmarks before optimisations and compare with benchstat.