1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
|
# 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 `<file>_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/<name>.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."
|