Skip to content

Table of Contents

Testing

Contents

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.

go test -run TestAbs/negative   # run one subtest

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.

// testify/mock style:
type MockStore struct{ mock.Mock }
func (m *MockStore) Get(id string) (*User, error) {
    args := m.Called(id)
    return args.Get(0).(*User), args.Error(1)
}
// m.On("Get", "1").Return(&User{}, nil); m.AssertExpectations(t)