Go Testing skill for Codex

Table tests, race-clean, runnable one case at a time. Codex writes Go tests in the standard-library style experienced Go teams expect: table-driven subtests with got/want messages, parallel where safe, golden files behind an -update flag, fuzz targets with committed corpora and synctest for timer-heavy code. It knows which features need which go.mod version and when testify helps or hurts.

  • Testing & QA
  • $go-testing
  • 235 lines
  • 2 files
  • ~2,000 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- go-testing

Installs to ~/.agents/skills/go-testing/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $go-testing, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Writes Go tests the idiomatic way - table-driven tests with t.Run subtests, t.Parallel, t.Helper and t.Cleanup, golden files, httptest, native fuzzing with testing.F, benchmarks with b.Loop, testing/synctest for time-dependent code, and when testify or go-cmp earns its place over the standard library. Use when adding or reviewing _test.go files, when go test -race fails, a subtest cannot be selected with -run, or a Go package needs fuzz or golden-file tests.

Use it when

  • Adding or reviewing _test.go files
  • Tests fail under -race or -shuffle=on
  • A package needs golden-file, fuzz or benchmark tests
  • Deciding between testify, go-cmp and the standard library

Use something else when

  • You need a long-running coverage-guided fuzz campaign across languages: use fuzz-testing
  • The goal is profiling a slow Go service rather than testing it: use go-pprof

How to check the result

go vet and go test -race -count=1 -shuffle=on ./... pass, and each new subtest can be run alone with -run using its underscored name.

The mistake it prevents

Using t.Setenv or shared package-level fakes inside parallel subtests, which panics or produces data races that only show up under -race.

~/.agents/skills/go-testing/SKILL.md

--- name: go-testing description: Writes Go tests the idiomatic way - table-driven tests with t.Run subtests, t.Parallel, t.Helper and t.Cleanup, golden files, httptest, native… license: MIT ---

Good Go tests are plain functions with table-driven cases, readable failure messages that show got and want, -race clean, parallel where state allows, and runnable one case at a time with -run.

Inspect first

  1. Read AGENTS.md and the Makefile/Taskfile for the canonical test target (tags, -race, -count=1, integration env vars).
  2. Check the go directive in go.mod. Features below are gated: loop-variable per iteration (1.22), b.Loop and t.Context (1.24), testing/synctest.Test (1.25).
  3. See whether the repo already uses testify, go-cmp, or neither. Follow the existing choice; do not mix assertion styles in one package.
  4. Note build tags such as //go:build integration that hide tests from a plain run.

Table-driven tests

func TestParseDuration(t *testing.T) {
	tests := []struct {
		name    string
		in      string
		want    time.Duration
		wantErr bool
	}{
		{name: "minutes", in: "5m", want: 5 * time.Minute},
		{name: "compound", in: "1h30m", want: 90 * time.Minute},
		{name: "empty string", in: "", wantErr: true},
		{name: "missing unit", in: "10", wantErr: true},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			got, err := ParseDuration(tt.in)
			if (err != nil) != tt.wantErr {
				t.Fatalf("ParseDuration(%q) error = %v, wantErr %v", tt.in, err, tt.wantErr)
			}
			if got != tt.want {
				t.Errorf("ParseDuration(%q) = %v, want %v", tt.in, got, tt.want)
			}
		})
	}
}
  • Failure messages follow Func(input) = got, want want. A reader should not need the source to understand the failure.
  • t.Fatalf when continuing is pointless (nil result); t.Errorf to report several mismatches in one run.
  • Subtest names become -run patterns with spaces turned into underscores: go test -run 'TestParseDuration/empty_string' ./....
  • Since Go 1.22 the loop variable is per-iteration, so tt := tt is no longer needed. Remove it only if go.mod says 1.22 or later.
  • For errors, compare with errors.Is(err, ErrNotFound) or errors.As, never string matching.

Helpers, cleanup, temp state

func newTestStore(t *testing.T) *Store {
	t.Helper()                               // failures point at the caller's line
	dir := t.TempDir()                       // removed automatically
	s, err := Open(filepath.Join(dir, "db"))
	if err != nil {
		t.Fatalf("open store: %v", err)
	}
	t.Cleanup(func() { s.Close() })
	return s
}
  • t.Setenv and t.Chdir change process state and panic in parallel tests; do not combine them with t.Parallel().
  • t.Context() (1.24) returns a context canceled just before cleanup; use it instead of context.Background() so leaked goroutines see cancellation.
  • TestMain(m *testing.M) only for package-wide setup such as starting one container; since Go 1.15 just call m.Run() and return, so deferred teardown still runs (os.Exit would skip it).

Comparing values

NeedUse
Scalars, small structs== or reflect.DeepEqual with a clear message
Structs, slices, maps with a readable diffgithub.com/google/go-cmp/cmp: if d := cmp.Diff(want, got); d != "" { t.Errorf("mismatch (-want +got):\n%s", d) }
Ignore fields, sort slicescmpopts.IgnoreFields, cmpopts.SortSlices, cmpopts.EquateApproxTime
Team already uses testifyrequire for preconditions that must stop the test, assert for checks; never use assert where a nil dereference follows

Avoid testify/suite for new code: it fights t.Parallel and subtests. Avoid testify/mock expectations for simple interfaces; a hand-written fake struct is shorter and type-checked.

HTTP handlers and clients

func TestGetUser(t *testing.T) {
	srv := NewServer(fakeRepo{users: map[string]User{"42": {ID: "42", Name: "Ada"}}})

	req := httptest.NewRequest(http.MethodGet, "/users/42", nil)
	rec := httptest.NewRecorder()
	srv.ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("status = %d, want 200; body: %s", rec.Code, rec.Body)
	}
}

For clients, start httptest.NewServer(handler) and point the client at srv.URL; close it with t.Cleanup(srv.Close).

Golden files

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

func TestRender(t *testing.T) {
	got := Render(input)
	golden := filepath.Join("testdata", t.Name()+".golden")
	if *update {
		os.MkdirAll(filepath.Dir(golden), 0o755)
		os.WriteFile(golden, got, 0o644)
	}
	want, err := os.ReadFile(golden)
	if err != nil {
		t.Fatal(err)
	}
	if d := cmp.Diff(string(want), string(got)); d != "" {
		t.Errorf("Render mismatch (-want +got):\n%s", d)
	}
}

Run go test ./pkg -run TestRender -update, then review the diff in git diff testdata/ before committing. Never update goldens just to make CI green.

Time-dependent code

Inject a clock interface first. For code that uses time.Sleep, timers or tickers internally, Go 1.25’s testing/synctest runs the test in a bubble with a fake clock:

func TestCacheExpires(t *testing.T) {
	synctest.Test(t, func(t *testing.T) {
		c := NewCache(time.Minute)
		c.Set("k", "v")
		time.Sleep(61 * time.Second)   // advances instantly inside the bubble
		synctest.Wait()                // let background goroutines settle
		if _, ok := c.Get("k"); ok {
			t.Fatal("entry should have expired")
		}
	})
}

On Go 1.24 the experimental API needs GOEXPERIMENT=synctest and uses synctest.Run.

Fuzzing

func FuzzParseDuration(f *testing.F) {
	for _, seed := range []string{"5m", "1h30m", "", "-0s"} {
		f.Add(seed)
	}
	f.Fuzz(func(t *testing.T, s string) {
		d, err := ParseDuration(s)
		if err != nil {
			return
		}
		if back, err := ParseDuration(d.String()); err != nil || back != d {
			t.Errorf("round trip %q -> %v -> %v (%v)", s, d, back, err)
		}
	})
}
go test -run=^$ -fuzz=FuzzParseDuration -fuzztime=60s ./pkg/timeparse

Failing inputs land in testdata/fuzz/FuzzParseDuration/; commit them so they run as regular cases in every go test. For long campaigns and corpus management, hand off to $fuzz-testing.

Benchmarks

func BenchmarkEncode(b *testing.B) {
	payload := makePayload()
	for b.Loop() {                 // Go 1.24+: setup excluded, result kept alive
		Encode(payload)
	}
}

Compare runs with go test -bench=Encode -count=10 > new.txt and benchstat old.txt new.txt (benchstat needs go install golang.org/x/perf/cmd/benchstat@latest, which requires network).

Running

go test ./...                                   # all packages
go test -race -count=1 -shuffle=on ./...        # no cache, random order, race detector
go test -run 'TestParseDuration/compound' -v ./pkg/timeparse
go test -tags=integration ./...                 # include tagged tests
go test -short ./...                            # tests check testing.Short() to skip slow paths
go test -coverprofile=cover.out ./... && go tool cover -func=cover.out

-count=1 disables the test cache; use it when tests read files or env vars that Go does not track.

Gotchas

  • t.Parallel() in the parent and subtests: the parent’s deferred cleanup runs before parallel subtests finish. Use t.Cleanup, which waits.
  • Calling t.Fatal from a goroutine other than the test’s does not stop the test; send errors back on a channel.
  • Shared package-level maps or fakes plus t.Parallel() produce -race failures; build fresh fakes per subtest.
  • External test packages (package foo_test) test the public API; internal ones (package foo) can reach unexported code. Prefer _test packages for behavior tests.

Verification

  1. go vet ./... and go test -race -count=1 -shuffle=on ./... pass.
  2. Each new subtest can be run alone via -run.
  3. Break the behavior under test once and confirm the message shows got and want clearly.
  4. Fuzz targets run at least 30 seconds locally without new failures.

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Go Testing"
  short_description: "Table tests, race-clean, runnable one case at a time"
  default_prompt: "$go-testing Write table-driven tests for this Go parser, including error cases"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc