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.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- go-testing Installs to ~/.agents/skills/go-testing/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install go-testing The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project go-testing Installs to .agents/skills/go-testing/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/go-testing.zipunzip -o go-testing.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) go-testing PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
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.
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
- Read
AGENTS.mdand theMakefile/Taskfilefor the canonical test target (tags,-race,-count=1, integration env vars). - Check the
godirective ingo.mod. Features below are gated: loop-variable per iteration (1.22),b.Loopandt.Context(1.24),testing/synctest.Test(1.25). - See whether the repo already uses testify, go-cmp, or neither. Follow the existing choice; do not mix assertion styles in one package.
- Note build tags such as
//go:build integrationthat 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.Fatalfwhen continuing is pointless (nil result);t.Errorfto report several mismatches in one run.- Subtest names become
-runpatterns with spaces turned into underscores:go test -run 'TestParseDuration/empty_string' ./.... - Since Go 1.22 the loop variable is per-iteration, so
tt := ttis no longer needed. Remove it only ifgo.modsays 1.22 or later. - For errors, compare with
errors.Is(err, ErrNotFound)orerrors.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.Setenvandt.Chdirchange process state and panic in parallel tests; do not combine them witht.Parallel().t.Context()(1.24) returns a context canceled just before cleanup; use it instead ofcontext.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 callm.Run()and return, so deferred teardown still runs (os.Exitwould skip it).
Comparing values
| Need | Use |
|---|---|
| Scalars, small structs | == or reflect.DeepEqual with a clear message |
| Structs, slices, maps with a readable diff | github.com/google/go-cmp/cmp: if d := cmp.Diff(want, got); d != "" { t.Errorf("mismatch (-want +got):\n%s", d) } |
| Ignore fields, sort slices | cmpopts.IgnoreFields, cmpopts.SortSlices, cmpopts.EquateApproxTime |
| Team already uses testify | require 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. Uset.Cleanup, which waits.- Calling
t.Fatalfrom 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-racefailures; build fresh fakes per subtest. - External test packages (
package foo_test) test the public API; internal ones (package foo) can reach unexported code. Prefer_testpackages for behavior tests.
Verification
go vet ./...andgo test -race -count=1 -shuffle=on ./...pass.- Each new subtest can be run alone via
-run. - Break the behavior under test once and confirm the message shows got and want clearly.
- 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.yaml
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.