Configures and runs Go's stdlib `testing` package - `func TestXxx(t *testing.T)` convention; table-driven tests via slice + range; `t.Run` for subtests with hierarchical names; `t.Parallel()` for parallel execution; benchmarks (`func BenchmarkXxx(b *testing.B)`); examples (`func ExampleXxx()`); fuzzing (`func FuzzXxx(f *testing.F)`); coverage via `go test -cover`/`-coverprofile`; build tags for selective compilation. Use for any Go project - testing is a stdlib feature, no install needed.
How this skill is triggered — by the user, by Claude, or both
Slash command
/qa-unit-tests-go-rust:go-testThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Per [pkg.go.dev/testing][go-test-pkg]:
Per pkg.go.dev/testing:
Go's testing package is stdlib - no separate install, no
configuration file. The single binary go test discovers, builds,
and runs tests via convention.
Distinguishing properties:
_test.go suffix; TestXxx / BenchmarkXxx
/ FuzzXxx / ExampleXxx function-name prefixes.// math.go
package math
func Add(a, b int) int {
return a + b
}
// math_test.go
package math
import "testing"
func TestAdd(t *testing.T) {
if got := Add(1, 2); got != 3 {
t.Errorf("Add(1, 2) = %d; want 3", got)
}
}
Run:
go test ./... # all packages recursively
go test ./mypackage # specific package
go test -v # verbose
go test -run TestAdd # specific test by name pattern
Per pkg.go.dev/testing#hdr-Subtests_and_Sub_benchmarks:
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b, expected int
}{
{"positive", 1, 2, 3},
{"zero", 0, 0, 0},
{"negative", -1, 1, 0},
{"large", 100, 200, 300},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Add(tt.a, tt.b); got != tt.expected {
t.Errorf("Add(%d, %d) = %d; want %d", tt.a, tt.b, got, tt.expected)
}
})
}
}
t.Run creates subtests with hierarchical naming (TestAdd/positive,
TestAdd/zero, etc.). Subtests are filterable + reportable
individually.
func TestSomethingSlow(t *testing.T) {
t.Parallel() // marks this test as parallel-safe
// ... slow operation
}
Parallel tests run concurrently with other parallel tests in the
same package. Tests without t.Parallel() run sequentially.
For subtests:
for _, tt := range tests {
tt := tt // capture loop variable (pre-Go 1.22; not needed Go 1.22+)
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
// ...
})
}
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(1, 2)
}
}
Run:
go test -bench=. # all benchmarks
go test -bench=BenchmarkAdd # specific
go test -bench=. -benchmem # memory allocation tracking
go test -bench=. -benchtime=10s # 10s per benchmark
go test -bench=. -count=5 # 5 runs each (use with benchstat)
For statistical comparison: benchstat tool compares two benchmark
runs:
go test -bench=. -count=10 > old.txt
# make changes
go test -bench=. -count=10 > new.txt
benchstat old.txt new.txt
func ExampleAdd() {
fmt.Println(Add(1, 2))
// Output: 3
}
The // Output: comment is the assertion. go test runs the
example + verifies output matches. Examples appear in go doc.
func FuzzAdd(f *testing.F) {
f.Add(1, 2) // seed corpus
f.Add(0, 0)
f.Add(-1, 1)
f.Fuzz(func(t *testing.T, a, b int) {
c := Add(a, b)
if c-a != b {
t.Errorf("Add(%d, %d) = %d; expected commutativity", a, b, c)
}
})
}
Run:
go test -fuzz=FuzzAdd # generates random inputs
go test -fuzz=FuzzAdd -fuzztime=30s # bounded fuzz time
Failures cached at testdata/fuzz/FuzzAdd/; subsequent go test
runs replay those cases as regression tests.
go test -cover # summary
go test -coverprofile=coverage.out # detailed
go tool cover -html=coverage.out # browser view
go tool cover -func=coverage.out # per-function
# Per-package coverage with all packages:
go test -coverprofile=coverage.out -coverpkg=./... ./...
Coverage threshold gating (no built-in flag; use shell):
go test -coverprofile=coverage.out ./...
COVERAGE=$(go tool cover -func=coverage.out | grep total | awk '{print substr($3, 1, length($3)-1)}')
if (( $(echo "$COVERAGE < 80" | bc -l) )); then
echo "Coverage $COVERAGE% below 80% threshold"
exit 1
fi
//go:build integration
package mypackage
import "testing"
func TestIntegration(t *testing.T) {
// only compiled with -tags=integration
}
Run:
go test -tags=integration ./...
Build tags allow per-environment test suites (unit / integration / e2e) without separate folder structure.
func setupTest(t *testing.T) *Database {
t.Helper() // marks this as helper for cleaner failure stacks
db := openTestDB()
t.Cleanup(func() {
db.Close()
})
return db
}
func TestUserCreation(t *testing.T) {
db := setupTest(t)
// ...
}
t.Helper() makes failure messages point to the caller, not the
helper. t.Cleanup runs after the test (replaces defer for
test-scoped resources).
- run: go test -race -coverprofile=coverage.out -v ./...
- uses: codecov/codecov-action@v4
with: { files: coverage.out }
-race enables the race detector (catches data races at test
time). Standard practice for any Go project with concurrency.
For JUnit XML output (consumable by junit-xml-analysis):
go install github.com/jstemmer/go-junit-report/v2@latest
go test -v ./... | go-junit-report > junit.xml
| Anti-pattern | Why it fails | Fix |
|---|---|---|
Skip t.Parallel() everywhere | Slow test suite at scale | Mark parallel-safe tests (Step 3) |
| Forget loop-variable capture pre-Go 1.22 | All subtests share last iteration's value | tt := tt (Step 3) |
Skip -race in CI | Race conditions ship to prod | Always -race (Step 10) |
Use assert from testify everywhere | Stdlib is sufficient + idiomatic | Plain if got != want { t.Errorf } |
Multiple t.Errorf in single sub-condition | Fail-fast loses context | One assertion per logical thing |
testify/stretchr/testify
for richer matchers (and that's idiomatic in Go ecosystems).t.Cleanup + helper functions.gomock for codegen.testing package documentationgo test flagsginkgo-tests,
cargo-test,
rstest-tests - sister toolstest-code-conventionsnpx claudepluginhub testland/qa --plugin qa-unit-tests-go-rustProvides CDSS development patterns for drug interaction checking, dose validation, clinical scoring (NEWS2, qSOFA), and alert classification integrated into EMR workflows.