# Go Testing Rules Applies to: `**/*_test.go` Implements the core principles from `testing.md`. All rules there apply here — this file covers Go-specific patterns. ## Framework: the standard `testing` package Use the standard library `testing` package. Reach for a third-party assertion library (`testify`) only when a project already uses it; don't introduce it for new code. Plain `if got != want { t.Errorf(...) }` is the idiom, and it keeps the failure message under your control. Avoid full BDD frameworks (Ginkgo/Gomega) unless the project standardizes on them — they obscure the standard `go test` failure output. ## Table-Driven Tests Are the Default A table-driven test is how Go expresses the Normal / Boundary / Error categories from `testing.md` in one place. Each row is a case; `t.Run` gives each a named subtest so a failure points at the exact row. ```go func TestCartApplyDiscount(t *testing.T) { tests := []struct { name string coupon string want int wantErr bool }{ {"normal percentage off", "SAVE10", 90, false}, // Normal {"zero discount is a no-op", "SAVE0", 100, false}, // Boundary {"expired coupon rejected", "EXPIRED", 0, true}, // Error } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := ApplyDiscount(100, tt.coupon) if (err != nil) != tt.wantErr { t.Fatalf("ApplyDiscount() err = %v, wantErr = %v", err, tt.wantErr) } if !tt.wantErr && got != tt.want { t.Errorf("ApplyDiscount() = %d, want %d", got, tt.want) } }) } } ``` Use `t.Errorf` to record a failure and keep going (multiple assertions per case); use `t.Fatalf` when continuing would panic or test nothing useful (a `nil` you're about to dereference). ### Pairwise for Parameter-Heavy Functions When the table would need dozens of rows to cover the combinations of three or more parameters (feature flags × roles × shipping × payment), switch to combinatorial coverage via `/pairwise-tests`. It generates a minimal matrix hitting every 2-way interaction — usually 80-99% fewer rows than exhaustive — which you paste straight back into the table. See `testing.md` § Combinatorial Coverage for when to skip. ## Run With the Race Detector Run the suite with `-race` for anything that touches goroutines, channels, or shared state. The race detector catches data races that are invisible in a plain run and flaky in production. ``` go test -race ./... ``` Wire `-race` into the project's `make test`. A passing race-free run is the bar for concurrent code, not an optional extra. ## Test Naming and Location - Tests live in `_test.go` beside the code, in the same package (`package foo`) for white-box tests, or `package foo_test` for black-box tests that exercise only the exported API. Prefer black-box (`foo_test`) when you're testing the contract; it keeps tests honest about what's public. - Top-level test functions: `TestThing`, scenario carried by the subtest name via `t.Run("expired coupon rejected", ...)`. - Use `testing.T.Parallel()` on independent subtests to surface ordering assumptions and speed the suite. Capture the range variable first (`tt := tt`) on Go versions before 1.22. ## Error Behavior, Not Error Text Assert the error's identity, not its prose. Production messages get reworded; behavior doesn't. ```go got := Withdraw(account, 999) if !errors.Is(got, ErrInsufficientFunds) { t.Errorf("Withdraw() error = %v, want ErrInsufficientFunds", got) } ``` Use `errors.Is` for sentinel errors and `errors.As` for typed errors. Match on a substring only when a specific value must appear (the offending filename), never on the whole message. This mirrors `testing.md`'s error-behavior rule. ## Fixtures: `testdata/` and Golden Files - Put fixture inputs in a `testdata/` directory — the Go tool ignores it for builds, and it travels with the package. - For large expected outputs, use the golden-file pattern: compare against `testdata/.golden` and regenerate with a `-update` flag. ```go var update = flag.Bool("update", false, "update golden files") // ... if *update { os.WriteFile(golden, got, 0o644) } ``` - Use `t.TempDir()` for scratch directories and `t.Cleanup()` for teardown; both are removed automatically and survive `t.Parallel()`. ## Mocking at Boundaries — Via Interfaces Go's interfaces are the seam. Define a small interface the code under test depends on, and pass a fake in the test. Don't mock concrete types you own. ### Mock these (external boundaries): - Network: use `net/http/httptest.Server` for HTTP clients, not a mocked `http.Client` transport you hand-build. - Time: inject a `func() time.Time` clock rather than calling `time.Now()` directly in business logic. - Filesystem: accept an `fs.FS` (or `io.Reader`/`io.Writer`) so a test can pass `fstest.MapFS` or a buffer. - Third-party service clients: depend on a narrow interface the package defines, not the vendor's concrete client. ### Never mock these (internal domain): - Concrete structs and methods you own — call them directly with real inputs. - Pure functions (parsing, encoding, calculation) — those are the work. - The standard library's own behavior. If a test needs an elaborate fake to stand in for your own code, that's a design signal: extract a smaller interface or split the function (see `testing.md` § If Tests Are Hard to Write). ## Measuring Coverage — `make coverage-summary` The bundle ships a coverage summary at `.claude/scripts/coverage-summary.go` plus a Makefile fragment (`coverage-makefile.txt`) with `coverage` and `coverage-summary` targets. After `make coverage` runs the suite with `-coverprofile` and prints `go tool cover -func`'s per-function table, `make coverage-summary` prints a file-weighted project number and every source file absent from the profile. The number to watch is that missing-file count. `go test ./...` lists in-module packages in the profile (at 0% when untested), so the missing list is usually empty for in-module code — it earns its keep on build-tagged files and dirs outside the `./...` compilation. The summary weights by file and counts an absent file as 0%, so untested code stays visible instead of being averaged away. It doesn't reimplement the per-function table — `go tool cover -func` already prints that. Copy the fragment's targets into your own Makefile to adopt it; the bundle never edits your Makefile. ## What the Validate Hook Already Enforces A PostToolUse hook runs `gofmt` and `go vet` on every edited `.go` file (see the bundle's `CLAUDE.md`). Formatting and compile errors are caught at edit time, so tests don't need to re-assert them — write tests for behavior, not for "it compiles."