Built-in testing: go test, table tests, benchmarks, fuzzing.
Writing Tests with testing.T
Tests in Go are first-class — no external framework required. Put a _test.go
file next to the code under test, write functions starting with Test, and run
go test.
A first test
// math.go
package math
func Add(a, b int) int { return a + b }
// math_test.go
package math
import "testing"
func TestAdd(t *testing.T) {
got := Add(2, 3)
if got != 5 {
t.Errorf("Add(2,3) = %d; want 5", got)
}
}
Run with:
go test ./...
Test API basics
| Method | Use |
| ------------- | --------------------------------------------------------------- |
| t.Error(...) | Report a failure and continue (still runs the rest of the test). |
| t.Errorf(...) | Same with Sprintf formatting. |
| t.Fatal(...) | Report failure and stop this test. |
| t.Skip(...) | Skip the test (with a reason). |
| t.Helper() | Mark this function as a helper — failures show the caller's line. |
Table-driven tests
The idiomatic style for many similar cases: a slice of structs, looped over.
Real code uses t.Run(tc.name, func(t *testing.T) { ... }) so each row shows up
as its own test. The playground above just simulates it.
Useful flags
go test ./... # everything
go test -run TestAdd # filter by name (regex)
go test -v # verbose, prints each test
go test -race ./... # run race detector — catches data races
go test -cover ./... # coverage summary
go test -count=1 # disable result caching (always re-run)
The race detector (-race) is invaluable once you start writing goroutines.
Subtests with t.Run
t.Run gives each row of a table test its own name in the output, lets you
filter them (go test -run TestFoo/zero), and isolates t.Fatal to that subtest:
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
if got := Add(tc.a, tc.b); got != tc.want {
t.Errorf("Add(%d,%d) = %d; want %d", tc.a, tc.b, got, tc.want)
}
})
}
Setup, teardown, and t.Cleanup
For per-test cleanup that runs even when the test fails or panics, prefer
t.Cleanup over defer. It composes nicely across helpers:
func newTempFile(t *testing.T) string {
t.Helper()
f, _ := os.CreateTemp("", "test-*")
t.Cleanup(func() { os.Remove(f.Name()) })
return f.Name()
}
Test files & _test packages
foo_test.goin the same package gives you access to unexported names — "white-box" testing.- A file declared
package foo_testinstead can only see the exported API — "black-box". The two can coexist.