Testing & Engineering
the testing package, table-driven, benchmarks, httptest, Fuzz, toolchain
Unit Tests
Test files end in _test.go and live in the same directory as the code under test; the function signature is func TestXxx(t *testing.T), and the name must start with Test followed by an uppercase letter. The go command finds them automatically.
// add.go
package math
func Add(a, b int) int { return a + b }// add_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)
}
}$ go test ./...
$ go test -run TestAdd -v
$ go test -count=1 ./... # disable the test result cacheTable-Driven Tests + Subtests
Go's most common test pattern: put cases in a slice and use t.Run to make each case an independent subtest. On failure you see exactly which subcase broke, and you can run a single one with -run.
// add_test.go
package math
import "testing"
func TestAdd_Table(t *testing.T) {
tests := []struct {
name string
a, b, want int
}{
{"both zero", 0, 0, 0},
{"positive", 1, 2, 3},
{"negative", -1, -2, -3},
}
for _, tc := range tests {
tc := tc // must rebind for Go < 1.22
t.Run(tc.name, func(t *testing.T) {
t.Parallel() // run subtests in parallel
if got := Add(tc.a, tc.b); got != tc.want {
t.Errorf("got %d, want %d", got, tc.want)
}
})
}
}Test Helpers: Helper / Cleanup / TempDir
testing.T provides a set of tools to hide low-level detail, clean up resources automatically, and avoid pollution.
// db_test.go
package storage
import (
"path/filepath"
"testing"
)
type DB struct{ path string }
func Open(path string) (*DB, error) { return &DB{path: path}, nil }
func (d *DB) Close() error { return nil }
func mustOpenDB(t *testing.T) *DB {
t.Helper() // on failure, points at the caller rather than this line
dir := t.TempDir() // auto-deleted when the test ends
db, err := Open(filepath.Join(dir, "a.db"))
if err != nil {
t.Fatalf("open: %v", err)
}
t.Cleanup(func() { db.Close() }) // more reliable than defer
return db
}
func TestDBOpen(t *testing.T) {
db := mustOpenDB(t)
_ = db
}Parallel Tests and -race
t.Parallel() runs cases concurrently — faster with more CPUs; also add -race so the race detector surfaces concurrency bugs early.
$ go test -race -parallel=8 ./...The testdata Directory
go toolchain convention: a directory named testdata is ignored (not compiled, not vetted). It's the right place for test JSON / golden files.
// render_test.go
package render
import (
"bytes"
"flag"
"os"
"testing"
)
var update = flag.Bool("update", false, "rewrite golden files")
func Render(in []byte) []byte { return bytes.ToUpper(in) }
func TestRender(t *testing.T) {
in, err := os.ReadFile("testdata/input.md")
if err != nil {
t.Fatal(err)
}
want, _ := os.ReadFile("testdata/expected.html")
got := Render(in)
if !bytes.Equal(got, want) {
if *update { // rewrite golden files with -update
os.WriteFile("testdata/expected.html", got, 0o644)
return
}
t.Errorf("got:\n%s\nwant:\n%s", got, want)
}
}httptest: Testing HTTP Handlers
net/http/httptest provides a fake Server and a ResponseRecorder: neither server nor client tests need to bind a real port.
// api_test.go
package api
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
)
type User struct{ ID int }
func UserHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "user:", r.PathValue("id"))
}
func Fetch(url string) (User, error) {
resp, err := http.Get(url)
if err != nil {
return User{}, err
}
defer resp.Body.Close()
var u User
err = json.NewDecoder(resp.Body).Decode(&u)
return u, err
}
// test the handler
func TestUserHandler(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("GET /users/{id}", UserHandler)
req := httptest.NewRequest("GET", "/users/42", nil)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
if rec.Code != 200 {
t.Errorf("status = %d", rec.Code)
}
}
// test client code (the server it hits is fake)
func TestFetch(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, `{"ID":1}`)
}))
defer srv.Close()
got, err := Fetch(srv.URL)
if err != nil || got.ID != 1 {
t.Fatalf("Fetch = %+v, %v", got, err)
}
}Example Tests: Dual Purpose
An ExampleXxx function is both documentation (shown automatically on pkg.go.dev) and a test — go test compares the // Output: comment against the actual output.
// example_test.go
package math
import "fmt"
func ExampleAdd() {
fmt.Println(Add(1, 2))
// Output: 3
}Benchmarks
A BenchmarkXxx function takes *testing.B; it loops b.N times and Go adjusts N automatically until the measurement is stable. -benchmem reports allocations and bytes per operation.
// bench_test.go
package math
import (
"strings"
"testing"
)
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = Add(1, 2)
}
}
func BenchmarkJoin(b *testing.B) {
parts := []string{"a", "b", "c"}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = strings.Join(parts, "-")
}
}$ go test -bench=. -benchmem -benchtime=2s
$ go test -bench=Add -cpuprofile=cpu.out
$ go tool pprof cpu.out # enter interactive analysisFuzz Testing (Go 1.18+)
A FuzzXxx function takes *testing.F: register seeds with Add, then Fuzz(func(t, ...)) lets Go mutate inputs automatically to find crashes and unexpected panics. Especially good for parsers and protocol layers.
// fuzz_test.go
package math
import (
"strconv"
"testing"
)
func FuzzParseInt(f *testing.F) {
f.Add("42")
f.Add("-1")
f.Add("")
f.Fuzz(func(t *testing.T, s string) {
v, err := strconv.Atoi(s)
if err == nil {
if got := strconv.Itoa(v); got != s {
t.Errorf("round trip: %q -> %d -> %q", s, v, got)
}
}
})
}$ go test -fuzz=FuzzParseInt -fuzztime=30sCoverage
$ go test -cover ./...
$ go test -coverprofile=cov.out && go tool cover -html=cov.out
$ go test -coverpkg=./... ./... # include cross-package callsMocks and Dependency Injection
Go recommends injecting dependencies via interfaces; in tests, pass a simple stub struct — no mock framework needed. If an interface has many methods, you can generate them with gomock / mockery.
// notify_test.go
package app
import "testing"
type User struct{ Email string }
type Mailer interface {
Send(to, body string) error
}
type service struct{ mail Mailer }
func (s *service) Notify(u User) error {
return s.mail.Send(u.Email, "hello")
}
// test stub
type fakeMail struct{ sent []string }
func (f *fakeMail) Send(to, body string) error {
f.sent = append(f.sent, to)
return nil
}
func TestNotify(t *testing.T) {
m := &fakeMail{}
s := &service{mail: m}
s.Notify(User{Email: "a@b.c"})
if len(m.sent) != 1 {
t.Fatal("not sent")
}
}Engineering Toolchain
- gofmt / goimports: the one formatting standard; run automatically on editor save
- go vet: built-in static checks
- staticcheck / golangci-lint: aggregate multiple linters, a CI staple
- go build -ldflags "-s -w -X main.version=$(git describe)": slim down + inject version
- go install puts a command into $GOBIN; go run runs a main package directly
- cross-compilation: GOOS=linux GOARCH=amd64 go build -o app
- go generate: trigger code generation (stringer / mockgen / sqlc, etc.)
- go tool pprof / trace: profiling and scheduler visualization