targetModels
DeepSeek V4DeepSeek V3.2DeepSeek R1DeepSeek V3 FamilyFuture DeepSeek Models
name
go-testing
category
Testing
description
Testing in Go with the standard toolchain — table-driven subtests, t.Helper, httptest and fstest, golden files, benchmarks, the race detector, fuzzing, and interfaces instead of mock frameworks.
license
MIT
author
Agent.md maintainers
last-verified
reviewed-by
unreviewed
#Task boundary
- Implement exactly the task as stated. Do not add abstractions, options, config, or files the task did not name.
- Comments, identifiers, commit messages and log strings are English only.
- Stop when the checklist at the end passes. Do not refactor or "improve" surrounding code.
- Every checklist item below is backed by an assertion in a test or by pasted command output, never by a sentence.
#Purpose
Rules for tests in Go. The testing package is deliberately small; the
patterns below are how the community fills the gap without a framework.
Reach for a dependency only after the standard library has demonstrably
failed you.
General testing principles are Testing/unit; HTTP handler design is
Backend/go-http.
#Table-driven subtests
gofunc TestDiscount(t *testing.T) {
tests := []struct {
name string
qty int
want int
}{
{"below threshold", 9, 0},
{"at threshold", 10, 100},
{"above threshold", 11, 110},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if got := Discount(tc.qty); got != tc.want {
t.Errorf("Discount(%d) = %d, want %d", tc.qty, got, tc.want)
}
})
}
}
- One table, one
t.Runper case, named sogo test -run TestDiscount/at_thresholdisolates it. t.Errorfreports and continues;t.Fatalfstops the subtest. UseFatalonly when continuing is meaningless (setup failed).- The failure message states input, got, want.
"wrong result"tells the reader nothing at 3am. t.Parallel()inside subtests runs cases concurrently and is the cheapest way to make-racesee something. From 1.22 the loop variable is per-iteration; before that,tc := tc.
#Helpers and cleanup
gofunc newStore(t *testing.T) *Store {
t.Helper() // failures point at the caller
db := openTestDB(t)
t.Cleanup(func() { db.Close() }) // runs after the test, in LIFO order
return NewStore(db)
}
t.Helper()as the first line of every helper that can fail. Without it, the reported line is inside the helper, not the test.t.Cleanupinstead ofdeferin helpers:deferruns when the helper returns, which is before the test uses the thing.t.TempDir()for files — created per test, removed automatically.t.Setenvsets an environment variable for the test's duration and is incompatible witht.Parallel()by design; if you need both, inject config instead of reading the environment.
#HTTP handlers with httptest
gofunc TestGetOrder(t *testing.T) {
h := New(fakeStore{orders: map[string]Order{"o1": {ID: "o1"}}}, slog.Default())
req := httptest.NewRequest(http.MethodGet, "/orders/o1", nil)
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want %d; body: %s", rec.Code, http.StatusOK, rec.Body)
}
}
httptest.NewRequest+httptest.NewRecordertest a handler with no port and no network. Use this for every handler test.httptest.NewServeris for testing an HTTP client against a real listener. Do not use it to test your own handlers.- Assert the denial cases — unauthenticated, wrong tenant, malformed body — before the happy path. They are the tests that catch a missing check.
#Fakes over mocks
go// The consumer's interface is small, so a fake is ten lines. No framework.
type fakeStore struct{ orders map[string]Order }
func (f fakeStore) Get(_ context.Context, id string) (Order, error) {
o, ok := f.orders[id]
if !ok { return Order{}, ErrNotFound }
return o, nil
}
- Small consumer-side interfaces make hand-written fakes trivial. A generated mock with call-count assertions couples the test to the implementation.
- Fake the boundary you own (
Store), not the library underneath (*sql.DB). For the database itself, run a real one in Docker:Testing/integration. testing/fstest.MapFSfakes a filesystem;io.Readerfromstrings.NewReaderfakes input. Design for interfaces and most mocking needs disappear.
#Golden files
govar 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.WriteFile(golden, got, 0o644)
}
want, _ := os.ReadFile(golden)
if !bytes.Equal(got, want) {
t.Errorf("output differs from %s\n%s", golden, diff(want, got))
}
}
- Large expected outputs (rendered templates, generated code, JSON) live in
testdata/, whichgo buildignores by convention. go test -updateregenerates them; the diff in the PR is the review.
#Benchmarks, race, fuzz
gofunc BenchmarkParse(b *testing.B) {
for i := 0; i < b.N; i++ { // or `for range b.N` from 1.24
Parse(sample)
}
}
func FuzzParse(f *testing.F) {
f.Add("valid input")
f.Fuzz(func(t *testing.T, s string) {
_, _ = Parse(s) // must not panic on any input
})
}
go test -bench . -benchmemreports ns/op and allocs/op. Runbenchstaton before/after output; a single run is noise.go test -race ./...in CI, always. →Backend/go-concurrency- Fuzz anything that parses untrusted bytes.
go test -fuzz FuzzParsefor ten minutes finds panics a table never would; keep found crashers intestdata/fuzz/as regression cases.
#Anti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
One giant test function with if chains | First failure hides the rest | Table-driven subtests |
Helper without t.Helper() | Failure points into the helper | First line of every helper |
defer cleanup inside a helper | Runs before the test uses the resource | t.Cleanup |
httptest.NewServer for handler tests | Slow, port-bound, unnecessary | NewRequest + NewRecorder |
Mocking *sql.DB | Tests your guess about the driver | Fake your Store; real DB for integration |
| Mock framework with call counts | Couples tests to implementation | Hand-written fakes |
time.Sleep to wait for goroutines | Flaky under load | Channels, sync.WaitGroup, or errgroup |
Reading time.Now() in code under test | Non-deterministic | Inject a clock |
t.Setenv with t.Parallel() | Panics — by design | Inject config |
Skipping -race | Data races ship | Gate CI on it |
| Golden files edited by hand | Drift from real output | -update flag regenerates them |
| Benchmarking once and trusting the number | Noise | benchstat across runs |
#Checklist
- Tests are table-driven with named
t.Runsubtests - Failure messages include input, got, and want
- Subtests call
t.Parallel()where the code under test is pure or safe - Every helper starts with
t.Helper()and usest.Cleanup - Temporary files use
t.TempDir() - Handlers are tested with
httptest.NewRequestandNewRecorder - Denial and error paths are asserted, not only the happy path
- Fakes implement the consumer's small interface; no
*sql.DBmocks - Large expected outputs are golden files under
testdata/with an-updateflag -
go test -race ./...runs in CI - Parsers of untrusted input have a
Fuzztarget with crashers kept as regressions - Benchmarks are compared with
benchstat, never a single run