Skip to content
gohacks.dev
Testing

Go Test Cheat Sheet for Everyday Testing Work in One Place

A Go testing cheat sheet: table tests, helpers, temp dirs, env, handlers, skips, parallel, benchmarks, fuzz and examples, with the commands that run each.

6 min read
Summarize in AI
Flat illustration of a test report with one failing test.

This is the page to keep open while writing tests. Each section is a pattern, a short reason, and the command that exercises it. Everything ran on Go 1.25.6 from the file below; the deeper articles are linked where a pattern has depth.

Tested with Go 1.25.6, macOS on Apple Silicon (arm64)

cheat_test.goGo
package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"testing"
)

func double(n int) int { return n * 2 }

// 1. table test with named cases
func TestTable(t *testing.T) {
	for _, c := range []struct {
		name     string
		in, want int
	}{{"zero", 0, 0}, {"pos", 2, 4}, {"neg", -3, -6}} {
		t.Run(c.name, func(t *testing.T) {
			if got := double(c.in); got != c.want {
				t.Errorf("double(%d) = %d, want %d", c.in, got, c.want)
			}
		})
	}
}

// 2. helper: failures point at the caller
func mustDouble(t *testing.T, in, want int) {
	t.Helper()
	if got := double(in); got != want {
		t.Fatalf("double(%d) = %d, want %d", in, got, want)
	}
}

func TestHelper(t *testing.T) { mustDouble(t, 4, 8) }

// 3. temp dir, cleanup, env
func TestTempAndEnv(t *testing.T) {
	dir := t.TempDir() // removed automatically
	path := filepath.Join(dir, "note.txt")
	if err := os.WriteFile(path, []byte("hi"), 0o600); err != nil {
		t.Fatal(err)
	}
	t.Setenv("APP_MODE", "test") // restored automatically
	t.Cleanup(func() { t.Logf("cleanup runs last, file existed: %v", fileExists(path)) })
	t.Logf("APP_MODE=%s", os.Getenv("APP_MODE"))
}

func fileExists(p string) bool { _, err := os.Stat(p); return err == nil }

// 4. HTTP handler without a server
func TestHandler(t *testing.T) {
	h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusTeapot) })
	rec := httptest.NewRecorder()
	h.ServeHTTP(rec, httptest.NewRequest("GET", "/", nil))
	t.Logf("status=%d", rec.Code)
}

// 5. skip
func TestSkipShort(t *testing.T) {
	if testing.Short() {
		t.Skip("slow")
	}
	t.Log("full mode")
}

// 6. parallel subtests
func TestParallel(t *testing.T) {
	for _, n := range []int{1, 2, 3} {
		t.Run("n", func(t *testing.T) {
			t.Parallel()
			if double(n) != n+n {
				t.Fail()
			}
		})
	}
}

// 7. benchmark and 8. fuzz and 9. example
func BenchmarkDouble(b *testing.B) {
	for b.Loop() {
		double(21)
	}
}

func FuzzDouble(f *testing.F) {
	f.Add(3)
	f.Fuzz(func(t *testing.T, n int) {
		if double(n) != n+n {
			t.Fatal("not additive")
		}
	})
}

func Example_double() {
	fmt.Println(double(4))
	// Output: 8
}

func main() {}
Shell
G='s/ \([0-9.]+s\)//; s#^ +[a-z_0-9]+\.go:[0-9]+: #    #; s/\t[0-9.]+s$//'
echo '$ go test -v -short'; go test -count=1 -v -short . 2>&1 | grep -vE '^(=== |PASS)' | sed -E "$G" | head -30
echo; echo '$ go test -run "TestTable/(pos|neg)" -v'; go test -count=1 -run 'TestTable/(pos|neg)' -v . 2>&1 | grep -E '^\s*---' | sed -E "$G"
echo; echo '$ go test -run "^$" -bench Double -benchtime 100x'; go test -count=1 -run '^$' -bench Double -benchtime 100x . 2>&1 | grep -E '^Benchmark' | sed -E 's/[[:space:]]+[0-9.]+ ns\/op.*/ ... ns\/op/'
echo; echo '$ go test -run "^$" -fuzz FuzzDouble -fuzztime 200x'; go test -count=1 -run '^$' -fuzz FuzzDouble -fuzztime 200x . 2>&1 | grep -E 'PASS|ok' | sed -E 's/\t[0-9.]+s//' | head -2
Text
$ go test -v -short
--- PASS: TestTable
    --- PASS: TestTable/zero
    --- PASS: TestTable/pos
    --- PASS: TestTable/neg
--- PASS: TestHelper
    APP_MODE=test
    cleanup runs last, file existed: true
--- PASS: TestTempAndEnv
    status=418
--- PASS: TestHandler
    slow
--- SKIP: TestSkipShort
--- PASS: TestParallel
    --- PASS: TestParallel/n
    --- PASS: TestParallel/n#01
    --- PASS: TestParallel/n#02
--- PASS: FuzzDouble
    --- PASS: FuzzDouble/seed#0
--- PASS: Example_double
ok  	example.test/lab

$ go test -run "TestTable/(pos|neg)" -v
--- PASS: TestTable
    --- PASS: TestTable/pos
    --- PASS: TestTable/neg

$ go test -run "^$" -bench Double -benchtime 100x
BenchmarkDouble-10    	     100 ... ns/op

$ go test -run "^$" -fuzz FuzzDouble -fuzztime 200x
PASS
ok  	example.test/lab

The patterns

I want toWriteNotes
Test many inputsA slice of named cases and t.Run(name, ...)The name is the failure label. See table tests
Share an assertionA function taking *testing.T that calls t.Helper() firstFailures point at the caller's line
Get a scratch directoryt.TempDir()Removed when the test ends
Set an environment variablet.Setenv(k, v)Restored after; panics in parallel tests
Change directoryt.Chdir(dir)Go 1.24; restored after; not for parallel tests
Run code at the endt.Cleanup(func(){...})Last in, first out
Test a handlerhttptest.NewRequest and httptest.NewRecorderNo port. See the recorder article
Skip a slow testif testing.Short() { t.Skip("reason") }Run with -short to skip
Run subtests togethert.Parallel() inside each t.RunCases must not share state
Measure speedfunc BenchmarkX(b *testing.B) with for b.Loop()See benchmarks
Find odd inputsfunc FuzzX(f *testing.F) with f.Add seedsSee fuzzing
Document and checkfunc ExampleX() ending in // Output:See example tests
Control timeA clock parameter, or synctest.TestSee clock tests
Fail and stopt.FatalfOnly from the test goroutine
Fail and continuet.ErrorfUse in loops and goroutines
Log for failurest.LogfShown on failure or with -v

What the run shows

Table test. TestTable produced three subtests named zero, pos and neg. Descriptive names make a failing row readable: TestTable/neg.

Helper and cleanup. TestTempAndEnv set APP_MODE=test, wrote a file into t.TempDir() and registered a cleanup. The cleanup logged file existed: true. That order is the rule: cleanups run before the temporary directory is removed, so a cleanup can still use files in it.

Handler. The recorder returned status=418 for a handler that writes the teapot status. No server was started.

Skip. With -short, TestSkipShort printed its reason and --- SKIP. Without the flag it would log full mode.

Parallel. Three subtests with the same name n were numbered n, n#01 and n#02. Go makes duplicate subtest names unique this way, which is a reason to give cases distinct names.

Fuzz as a test. Without -fuzz, FuzzDouble ran its seed once as FuzzDouble/seed#0. A fuzz target is a table test until you ask it to fuzz.

Example. Example_double printed 8 and matched its // Output: comment.

Selecting what runs

GoalCommand
One testgo test -run '^TestTable$'
Some subtestsgo test -run 'TestTable/(pos or neg)', written as a regular expression with alternation
Everything but somego test -skip 'Slow'
List without runninggo test -list .
Verbosego test -v
No cachego test -count=1
Short modego test -short
Race detectorgo test -race
Coveragego test -cover -coverprofile=c.out
One benchmarkgo test -run '^$' -bench Double -benchtime 100x
Fuzz for a whilego test -run '^$' -fuzz FuzzDouble -fuzztime 30s
Machine-readablego test -json
Fail fastgo test -failfast

The second row is a regular expression with an alternation of the two subtest names; the lab writes it with the vertical bar, quoted so the shell leaves it alone. The full flag list with notes is in the flags reference.

Habits worth keeping

  1. Name every case, and make the name say what is special about it.
  2. Put the input in the failure message: double(%d) = %d, want %d.
  3. Use t.Fatalf when the rest of the test is meaningless after a failure, t.Errorf when later checks are still informative.
  4. Keep the code under test out of the test's own logic; call it, compare, report.
  5. Let the framework clean up: t.TempDir, t.Setenv, t.Cleanup, t.Context.
  6. Run -race and -shuffle in CI, because both turn silent shared-state bugs into failures you can read.
  7. Keep slow and external tests behind a tag; see the build tag article.

Mistakes this sheet is meant to prevent

  • Anchoring. -run TestTable also matches TestTableSlow. Use ^TestTable$ when you mean one.
  • Forgetting the cache. A passing result may be replayed; add -count=1 when the test touches the outside world. See the cache article.
  • Sharing a loop variable. Fine since Go 1.22 in modules that declare it; copy it first on older ones.
  • Calling t.Fatal in a goroutine. Use t.Error and signal the test goroutine; go vet reports the mistake.
  • Asserting nothing. A test that cannot fail is worse than no test, as the mistakes roundup shows.

Where to go next

The patterns above are the base. For the surrounding practice, the testing guide maps the topics, and the Go documentation for the testing package is the full reference. The go test command documents the flags.

Share

Keep reading

Get new articles by email

We send one email when a new article is published. No spam.

Unsubscribe in one click. No spam, ever.