Go Cheatsheet

Testing

Use this Go reference while you build software engineering projects, review code, or refresh the syntax you reach for most.

Running Tests

go test is the whole test runner. No config file, no external framework.

go test ./...                       # every package, recursively
go test -v ./pkg/store              # verbose, print every t.Log
go test -run 'TestParse'            # regexp on the test name
go test -run 'TestParse/empty_input' # one subtest (spaces became underscores)
go test -skip 'TestSlow' ./...      # inverse of -run
go test -count=1 ./...              # the only way to bypass the test cache
go test -race ./...                 # data race detector, use it in CI
go test -timeout 30s ./...          # panic-dump the whole binary after 30s
go test -short ./...                # sets testing.Short()
go test -failfast ./...             # stop after the first failing test
go test -shuffle=on ./...           # randomize order, catches shared state
go test -json ./... > out.json      # machine-readable events
go test -list '.*' ./pkg/store      # list test names, run nothing

Coverage and benchmarks:

go test -cover ./...
go test -coverprofile=cover.out -covermode=atomic ./...
go tool cover -html=cover.out                 # annotated source in a browser
go tool cover -func=cover.out | tail -1       # total percentage

go test -bench=. -benchmem ./...              # all benchmarks, alloc stats
go test -bench=BenchmarkParse -benchtime=5s
go test -bench=BenchmarkParse -benchtime=1000x # exactly 1000 iterations
go test -fuzz=FuzzParse -fuzztime=30s ./pkg/parse

-covermode=atomic is required whenever you combine -race with coverage, since the default set mode is not race-safe. Test results are cached, and -count=1 is the documented way to force a rerun.

File and Package Layout

store/
  store.go
  store_test.go        // package store       — internal, sees unexported ids
  export_test.go       // package store       — expose internals to the ext test
  api_test.go          // package store_test  — external, only the public API
  testdata/            // ignored by the go tool, put fixtures here
    golden/parse.json

- A test file must end in _test.go and is excluded from the normal build. - package foo tests reach unexported identifiers. package foo_test in the same directory can only use the exported API, which is the honest way to test that your package is usable, and it breaks import cycles. - testdata is skipped by every go command, so fixtures live there. - Test function signature: func TestXxx(t *testing.T), and the name after Test must not start with a lowercase letter.

The testing.T API

MethodEffect
t.Error / Errorfmark failed, keep running
t.Fatal / Fatalfmark failed, stop this test now
t.Log / Logfoutput, shown only with -v or on failure
t.Skip / Skipf / SkipNowskip the rest of this test
t.Helper()report the caller's line, not this function's
t.Cleanup(fn)run fn when the test ends, LIFO, even on failure
t.TempDir()a fresh directory, removed automatically
t.Setenv(k, v)set an env var, restored after (forbids t.Parallel)
t.Chdir(dir)change the working dir, restored after (1.24+)
t.Context()a context cancelled just before Cleanup runs (1.24+)
t.Parallel()run this test alongside other parallel ones
t.Run(name, fn)a subtest
t.Name()the full test name, e.g. TestParse/empty
t.Deadline()the -timeout instant, if set
func TestWriteFile(t *testing.T) {
    dir := t.TempDir()                    // cleaned up for you
    t.Setenv("APP_ENV", "test")           // restored for you

    path := filepath.Join(dir, "out.txt")
    if err := WriteFile(path, "hi"); err != nil {
        t.Fatalf("WriteFile(%q) = %v, want nil", path, err)
    }
}

// Helpers must call t.Helper(), or every failure points at the helper
func mustDecode(t *testing.T, b []byte) Config {
    t.Helper()
    var c Config
    if err := json.Unmarshal(b, &c); err != nil {
        t.Fatalf("decode config: %v", err)
    }
    return c
}

t.Fatal calls runtime.Goexit, so it only works on the test's own goroutine. From a spawned goroutine use t.Error and return.

Table-Driven Tests

This is the canonical Go test. One slice of cases, one loop, one subtest each.

func TestParseDuration(t *testing.T) {
    tests := []struct {
        name    string
        in      string
        want    time.Duration
        wantErr bool
    }{
        {name: "seconds", in: "30s", want: 30 * time.Second},
        {name: "minutes", in: "5m", want: 5 * time.Minute},
        {name: "compound", in: "1h30m", want: 90 * time.Minute},
        {name: "empty", in: "", wantErr: true},
        {name: "garbage", in: "abc", wantErr: true},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            t.Parallel()
            got, err := ParseDuration(tt.in)
            if tt.wantErr {
                if err == nil {
                    t.Fatalf("ParseDuration(%q) = %v, want error", tt.in, got)
                }
                return
            }
            if err != nil {
                t.Fatalf("ParseDuration(%q) unexpected error: %v", tt.in, err)
            }
            if got != tt.want {
                t.Errorf("ParseDuration(%q) = %v, want %v", tt.in, got, tt.want)
            }
        })
    }
}

Message convention: got = X, want Y, with the input echoed. A failure should be readable without opening the test.

For structs and slices, compare with reflect.DeepEqual or, better, github.com/google/go-cmp/cmp:

if diff := cmp.Diff(tt.want, got); diff != "" {
    t.Errorf("Parse(%q) mismatch (-want +got):\n%s", tt.in, diff)
}

Subtests and Parallel

t.Run("group", func(t *testing.T) {
    t.Run("case a", func(t *testing.T) { /* ... */ })
    t.Run("case b", func(t *testing.T) { /* ... */ })
})

Subtest names get spaces replaced by underscores, so target them as -run 'TestParse/case_a'.

t.Parallel() pauses the subtest until its parent returns, then runs all paused siblings together, up to -parallel (default GOMAXPROCS).

- Pre Go 1.22, for _, tt := range tests shared one loop variable, so every parallel subtest saw the last case. The fix was tt := tt inside the loop. Go 1.22 made each iteration its own variable and the copy is now unnecessary, but only for modules declaring go 1.22 or later in go.mod. - Parent code after the t.Run loop runs before any parallel subtest finishes. Put shared teardown in t.Cleanup, never after the loop. - t.Setenv and t.Chdir panic in a parallel test, since they mutate process state.

TestMain, Fakes, and Golden Files

func TestMain(m *testing.M) {
    flag.Parse()                     // needed if you define custom flags
    if err := startDB(); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    code := m.Run()
    stopDB()                         // deferred functions do NOT run before Exit
    os.Exit(code)
}

Fakes come from interfaces, no mocking library required:

type Store interface {
    Get(ctx context.Context, id string) (Item, error)
}

type fakeStore struct {
    items map[string]Item
    err   error
}

func (f fakeStore) Get(_ context.Context, id string) (Item, error) {
    if f.err != nil {
        return Item{}, f.err
    }
    it, ok := f.items[id]
    if !ok {
        return Item{}, ErrNotFound
    }
    return it, nil
}

Golden files keep large expected output out of the source:

var update = flag.Bool("update", false, "rewrite golden files")

func TestRender(t *testing.T) {
    got := Render(input)
    golden := filepath.Join("testdata", "render.golden")

    if *update {
        if err := os.WriteFile(golden, got, 0o644); err != nil {
            t.Fatal(err)
        }
    }
    want, err := os.ReadFile(golden)
    if err != nil {
        t.Fatal(err)
    }
    if !bytes.Equal(got, want) {
        t.Errorf("Render mismatch, rerun with -update to accept:\n%s", got)
    }
}

Refresh them with go test ./... -update, then read the diff before committing.

Benchmarks

// Go 1.24+ : b.Loop handles timing and keeps the compiler honest
func BenchmarkParse(b *testing.B) {
    data := loadFixture(b)
    for b.Loop() {
        _, _ = Parse(data)
    }
}

// The classic form, still everywhere
func BenchmarkParseOld(b *testing.B) {
    data := loadFixture(b)
    b.ReportAllocs()
    b.ResetTimer()                   // drop setup cost from the measurement
    for i := 0; i < b.N; i++ {
        _, _ = Parse(data)
    }
}

func BenchmarkParallel(b *testing.B) {
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            _, _ = Parse(data)
        }
    })
}

b.Loop is the 1.24 replacement for the b.N loop. Setup before it is excluded from the timing automatically, so ResetTimer is unnecessary, and it prevents the compiler from optimizing away a call whose result you discard. Other tools: b.StopTimer/StartTimer around per-iteration setup, b.ReportMetric(v, "ops") for a custom unit, and benchstat on two -count=10 runs to know whether a delta is real.

Fuzzing

func FuzzParse(f *testing.F) {
    f.Add("30s")                     // seed corpus, types fix the fuzz signature
    f.Add("1h30m")
    f.Add("")

    f.Fuzz(func(t *testing.T, in string) {
        d, err := ParseDuration(in)
        if err != nil {
            return                   // rejecting bad input is correct
        }
        // property: round-tripping must be stable
        got, err := ParseDuration(d.String())
        if err != nil || got != d {
            t.Errorf("round trip %q -> %v -> %v, %v", in, d, got, err)
        }
    })
}

Run with go test -fuzz=FuzzParse -fuzztime=30s. Without -fuzz it only runs the seed corpus, so it behaves like a normal test in CI. A crash is written to testdata/fuzz/FuzzParse/, commit that file and it becomes a permanent regression case. Fuzz targets accept []byte, string, the numeric types, bool and rune only.

Examples

An Example function is documentation that the test runner verifies.

func ExampleParseDuration() {
    d, _ := ParseDuration("1h30m")
    fmt.Println(d)
    // Output: 1h30m0s
}

func ExampleClient_Get() {          // attaches to Client.Get in the docs
    // ...
    // Unordered output:
    // b
    // a
}

The // Output: comment is the assertion, compared after trimming leading and trailing space on each line. Drop the comment and the example compiles but is never run. Naming is Example, ExampleF, ExampleT, ExampleT_M, with an optional lowercase suffix like ExampleParseDuration_hours.

Assertions and Vet

The standard library gives you if and t.Errorf. That is the idiom.

// Errors: match by identity or type, never by string
if !errors.Is(err, ErrNotFound) {
    t.Fatalf("Get() error = %v, want ErrNotFound", err)
}

var perr *ParseError
if !errors.As(err, &perr) {
    t.Fatalf("Get() error = %T, want *ParseError", err)
}
if perr.Offset != 4 {
    t.Errorf("Offset = %d, want 4", perr.Offset)
}

go vet runs automatically as part of go test with a subset of its checks (printf, tests, atomic and a few more). Run the full set with go vet ./..., and add staticcheck for anything deeper.

github.com/stretchr/testify is very common and its require.NoError reads nicely. The standard library is genuinely enough, and a table test with plain if blocks has no dependency and no magic. Pick one per repo and stay consistent.