Table of Contents
- Testing
- Contents
- Basics
- Table-driven tests
- Subtests
- t.Parallel
- Helpers, setup & teardown
- stdlib assertions vs testify
- Benchmarks
- Fuzzing
- Examples
- Coverage
- httptest
- Mocks & interfaces
Testing¶
Contents¶
- Basics
- Table-driven tests
- Subtests
- t.Parallel
- Helpers, setup & teardown
- stdlib assertions vs testify
- Benchmarks
- Fuzzing
- Examples
- Coverage
- httptest
- Mocks & interfaces
Basics¶
Files end in _test.go; test funcs are TestXxx(t *testing.T). Package foo tests live in foo (white-box) or foo_test (black-box, external).
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) // Errorf continues; Fatalf stops
}
}
go test ./... # run all
go test -run TestAdd # regexp filter
go test -v # verbose
go test -count=1 # disable test result caching
t.Error/Errorf records failure and continues; t.Fatal/Fatalf stops the test. t.Log prints only with -v or on failure.
Table-driven tests¶
The idiomatic Go pattern — one test, many cases.
func TestAbs(t *testing.T) {
tests := []struct {
name string
in int
want int
}{
{"positive", 3, 3},
{"negative", -3, 3},
{"zero", 0, 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Abs(tt.in); got != tt.want {
t.Errorf("Abs(%d) = %d, want %d", tt.in, got, tt.want)
}
})
}
}
Subtests¶
t.Run(name, fn) creates a named subtest. Enables granular filtering and per-case parallelism.
t.Parallel¶
Mark a test (or subtest) to run concurrently with other parallel tests.
func TestX(t *testing.T) {
t.Parallel() // siblings calling t.Parallel run together
// ...
}
// Parallel subtests in a table loop (Go 1.22+ loop var is per-iteration):
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
// tt is captured correctly in Go 1.22+; pre-1.22 do tt := tt
})
}
-parallel N sets max parallelism (default GOMAXPROCS).
Helpers, setup & teardown¶
func mustOpen(t *testing.T, name string) *os.File {
t.Helper() // failures report the CALLER's line, not here
f, err := os.Open(name)
if err != nil { t.Fatalf("open: %v", err) }
t.Cleanup(func() { f.Close() }) // runs at end of test, LIFO
return f
}
// Package-level setup/teardown:
func TestMain(m *testing.M) {
setup()
code := m.Run() // runs the tests
teardown()
os.Exit(code)
}
t.TempDir() // auto-removed temp dir
t.Setenv("KEY", "val") // restored after test (forbids parallel)
t.Skip("reason"); t.Skipf("need %s", x)
stdlib assertions vs testify¶
Stdlib has no assert library — you write if got != want. Many teams add testify for terser assertions and mocks.
import (
"github.com/stretchr/testify/assert" // continues on failure
"github.com/stretchr/testify/require" // stops on failure (like Fatal)
)
func TestWithTestify(t *testing.T) {
got, err := Parse("42")
require.NoError(t, err) // abort if error
assert.Equal(t, 42, got) // (t, expected, actual)
assert.Len(t, list, 3)
assert.Contains(t, "hello", "ell")
assert.ErrorIs(t, err, ErrX)
assert.Eventually(t, cond, time.Second, 10*time.Millisecond)
}
Stdlib equivalent (no dependency):
if !reflect.DeepEqual(got, want) {
t.Errorf("got %v, want %v", got, want)
}
// Go 1.21+: use the experimental cmp for nicer diffs (github.com/google/go-cmp)
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
Rule of thumb: stdlib for libraries (zero deps); testify/cmp for app code where ergonomics win.
Benchmarks¶
BenchmarkXxx(b *testing.B); loop b.N times. The framework tunes b.N.
func BenchmarkFib(b *testing.B) {
for i := 0; i < b.N; i++ {
Fib(20)
}
}
func BenchmarkParse(b *testing.B) {
data := setup() // not timed if before the loop... but use:
b.ResetTimer() // exclude setup from timing
b.ReportAllocs() // include alloc stats
for i := 0; i < b.N; i++ {
Parse(data)
}
}
go test -bench=. # run all benchmarks
go test -bench=Fib -benchmem # include memory stats
go test -bench=. -benchtime=2s # min time per benchmark
go test -bench=. -count=10 > new.txt # for benchstat comparison
Use b.RunParallel to benchmark concurrent code. Prevent dead-code elimination by assigning results to a package-level sink.
Fuzzing¶
Built-in since Go 1.18. FuzzXxx(f *testing.F) with a seed corpus.
func FuzzReverse(f *testing.F) {
f.Add("hello") // seed corpus
f.Add("")
f.Fuzz(func(t *testing.T, s string) {
r := Reverse(Reverse(s))
if r != s {
t.Errorf("round-trip failed: %q != %q", r, s)
}
if !utf8.ValidString(Reverse(s)) {
t.Errorf("produced invalid UTF-8")
}
})
}
go test -run=Fuzz -fuzz=FuzzReverse # run the fuzzer
go test -fuzz=FuzzReverse -fuzztime=30s # bounded duration
# Failing inputs are saved under testdata/fuzz/ and replayed as regular tests.
Examples¶
ExampleXxx functions double as documentation AND tests when they have an // Output: comment.
func ExampleAdd() {
fmt.Println(Add(1, 2))
// Output: 3
}
func ExampleReverse_unordered() {
// Unordered output passes if lines match in any order:
// Unordered output:
// a
// b
}
go test verifies the printed output matches. Examples appear in go doc / pkg.go.dev.
Coverage¶
go test -cover # summary percentage
go test -coverprofile=cover.out ./... # write profile
go tool cover -func=cover.out # per-function coverage
go tool cover -html=cover.out # browser visualization
go test -covermode=atomic # for -race / parallel accuracy
httptest¶
Test HTTP handlers and clients without a real socket.
import "net/http/httptest"
// Test a handler:
func TestHandler(t *testing.T) {
req := httptest.NewRequest("GET", "/users/1", nil)
rr := httptest.NewRecorder()
Handler(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("status = %d", rr.Code)
}
if !strings.Contains(rr.Body.String(), "alice") {
t.Errorf("body = %q", rr.Body.String())
}
}
// Spin up a real test server for client code:
func TestClient(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
fmt.Fprintln(w, `{"ok":true}`)
}))
defer srv.Close()
resp, err := http.Get(srv.URL)
// ... assert on resp
}
Mocks & interfaces¶
Define narrow interfaces at the consumer; inject fakes in tests.
type Store interface {
Get(id string) (*User, error)
}
type Service struct{ store Store }
// Hand-written fake — often clearer than generated mocks:
type fakeStore struct {
users map[string]*User
}
func (f *fakeStore) Get(id string) (*User, error) {
u, ok := f.users[id]
if !ok { return nil, ErrNotFound }
return u, nil
}
func TestService(t *testing.T) {
svc := &Service{store: &fakeStore{users: map[string]*User{"1": {Name: "a"}}}}
// ...
}
Generated mocks: go.uber.org/mock (mockgen) or testify/mock.