Testing
Testing in Go is built into the language and its tooling. No separate
frameworks, no annotations, no configuration. Write a file ending in
_test.go, name your functions TestXxx, run
go test. That's it.
Learning objectives
- Write and run unit tests with the
testingpackage. - Structure tests using the table-driven pattern.
- Use
t.Runfor subtests. - Mark helpers with
t.Helper()for better stack traces. - Measure coverage and write HTTP handler tests with
httptest.
Your first test
math.go:
package mymath
func Add(a, b int) int { return a + b }
math_test.go:
package mymath
import "testing"
func TestAdd(t *testing.T) {
got := Add(2, 3)
if got != 5 {
t.Errorf("Add(2, 3) = %d, want 5", got)
}
}
Rules:
- File ends in
_test.go. - Function is named
Test+ CapitalName. - Takes a single
*testing.T. - Use
t.Errorfto fail without stopping,t.Fatalfto fail and stop.
Running tests
go test . # run this package
go test ./... # run every package in the module
go test -v ./... # verbose: show each test
go test -run TestAdd . # run only tests matching regex
go test -race ./... # with race detector (Chapter 24)
go test -timeout 30s ./... # set a timeout
Table-driven tests
Most idiomatic pattern in Go: a slice of test cases + a loop:
func TestAdd(t *testing.T) {
cases := []struct {
name string
a, b, want int
}{
{"positive", 2, 3, 5},
{"negative", -2, -3, -5},
{"zero", 0, 0, 0},
{"mix", -1, 5, 4},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
got := Add(c.a, c.b)
if got != c.want {
t.Errorf("Add(%d, %d) = %d, want %d", c.a, c.b, got, c.want)
}
})
}
}
Add a case → run it. No new test function, no copy-paste.
Subtests with t.Run
t.Run creates a named child test. Benefits:
- Clearer output (
TestAdd/positive,TestAdd/negative). - You can target a single case:
go test -run TestAdd/negative. - Failed subtests don't stop others.
Test helpers with t.Helper()
func assertEqual(t *testing.T, got, want any) {
t.Helper() // report errors at the CALL SITE, not inside this func
if !reflect.DeepEqual(got, want) {
t.Errorf("got %v, want %v", got, want)
}
}
Without t.Helper(), error messages point to the
assertion function. With it, they point to your actual test code.
Always call t.Helper() first in test helper functions.
t.Cleanup
func TestWithDB(t *testing.T) {
db := openTestDB(t)
t.Cleanup(func() { db.Close() }) // runs when THIS test (or subtest) exits
// ... use db ...
}
t.Cleanup is like defer but test-aware.
It runs after the test, in LIFO order, even on failure.
Example functions (doc + test in one)
func ExampleAdd() {
fmt.Println(mymath.Add(2, 3))
// Output: 5
}
The compiler runs the example, captures stdout, compares it to the
// Output: comment. The example also appears in the
package's generated documentation. Two birds, one function.
Coverage
go test -cover ./... # summary
go test -coverprofile=cov.out ./...; go tool cover -html=cov.out
The HTML report highlights which lines were executed: green for covered, red for not. Use it as a diagnostic, not a score (100% coverage with bad tests is worse than 60% with good ones).
HTTP handler tests with net/http/httptest
func TestHello(t *testing.T) {
req := httptest.NewRequest("GET", "/hello?name=Alice", nil)
rec := httptest.NewRecorder()
helloHandler(rec, req)
if rec.Code != 200 {
t.Errorf("status = %d, want 200", rec.Code)
}
if !strings.Contains(rec.Body.String(), "Alice") {
t.Errorf("body = %q, want to contain 'Alice'", rec.Body.String())
}
}
No real HTTP server required. ResponseRecorder captures
the output so you can assert on it. For integration-style tests,
httptest.NewServer spins up a real listener on a random
port.
Check your understanding
Practice exercises
Table-test a reverse function
Write Reverse(s string) string (from Chapter 7) and a table-driven test covering empty strings, single ASCII, multibyte ("héllo"), and emoji. Ensure the test fails if reverse is broken.
Show solution sketch
func TestReverse(t *testing.T) {
cases := []struct{ name, in, want string }{
{"empty", "", ""},
{"ascii", "abc", "cba"},
{"multibyte", "héllo", "olléh"},
{"emoji", "ab🌍", "🌍ba"},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
if got := Reverse(c.in); got != c.want {
t.Errorf("Reverse(%q) = %q, want %q", c.in, got, c.want)
}
})
}
}
Further reading
Tests in the bank.