# Testing

> Go testing — table-driven tests, test helpers, benchmarks, fuzz testing, profiling, and testify. TRIGGER when: user asks about Go testing, go test, testing package, table-driven tests in Go, Go test helpers, t.Helper, t.Run, Go benchmarks, Go profiling, pprof, Go fuzz testing, Go test coverage, testify, Go mock, Go test patterns, Go test organization, TestMain, t.Cleanup, t.Parallel, Go golden files, Go httptest. DO NOT USE when: user needs general testing philosophy (not Go-specific) — use `testing` skill instead.

- Skill: `bsene/testing-2` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add bsene/testing-2`
- Raw SKILL.md: https://api.skillmd.com/api/skills/bsene/testing-2/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: bsene (https://skillmd.com/u/bsene)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/bsene/testing-2

---


# Go Testing

Go has a built-in test framework — no external library needed. Files end in `_test.go`, functions start with `Test`, and `go test` runs everything.

**Always run `go test -race ./...` in CI** — the race detector catches data races that otherwise ship to production.

---

## Test File Conventions

| Convention    | Rule                                                       |
| ------------- | ---------------------------------------------------------- |
| File name     | `*_test.go` — same package, same directory                 |
| Test function | `func TestXxx(t *testing.T)` — uppercase after `Test`      |
| Benchmark     | `func BenchmarkXxx(b *testing.B)`                          |
| Fuzz test     | `func FuzzXxx(f *testing.F)`                               |
| Example       | `func ExampleXxx()` — with `// Output:` comment            |
| Test package  | Same package (white-box) or `package foo_test` (black-box) |

---

## Table-Driven Tests

The canonical Go testing pattern:

```go
func TestParseSize(t *testing.T) {
    tests := []struct {
        name    string
        input   string
        want    int64
        wantErr bool
    }{
        {name: "bytes", input: "100B", want: 100},
        {name: "kilobytes", input: "2KB", want: 2048},
        {name: "megabytes", input: "1MB", want: 1048576},
        {name: "empty", input: "", wantErr: true},
        {name: "invalid", input: "abc", wantErr: true},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got, err := ParseSize(tt.input)
            if tt.wantErr {
                if err == nil {
                    t.Fatal("expected error, got nil")
                }
                return
            }
            if err != nil {
                t.Fatalf("unexpected error: %v", err)
            }
            if got != tt.want {
                t.Errorf("ParseSize(%q) = %d, want %d", tt.input, got, tt.want)
            }
        })
    }
}
```

---

## Testing Decision Table

| Need                              | Tool / Approach                                   |
| --------------------------------- | ------------------------------------------------- |
| Multiple cases for one function   | Table-driven tests with `t.Run`                   |
| Shared setup / teardown           | `TestMain(m *testing.M)` or `t.Cleanup(func())`   |
| Mark helpers (clean stack traces) | `t.Helper()` as first line of helper function     |
| Run tests in parallel             | `t.Parallel()` at start of test/subtest           |
| Measure performance               | `go test -bench=. -benchmem`                      |
| Find edge-case inputs             | `go test -fuzz=FuzzXxx`                           |
| Profile CPU/memory                | `go test -cpuprofile cpu.prof` → `go tool pprof`  |
| Assert with richer API            | `testify/assert` or `testify/require`             |
| Test HTTP handlers                | `httptest.NewRecorder()` + `httptest.NewServer()` |
| Snapshot testing                  | Golden files in `testdata/`                       |
| Build tags for integration tests  | `//go:build integration`                          |

---

## t.Parallel() Loop-Variable Capture

Before Go 1.22, a parallel subtest in a `range` loop captures the loop variable by reference — all subtests see the final iteration's value. This is the same root cause covered in [Closures](../references/functions-methods-pointers.md#closures); pin it per iteration:

```go
for _, tt := range tests {
    tt := tt // pin (unnecessary on Go 1.22+, where range vars are per-iteration)
    t.Run(tt.name, func(t *testing.T) {
        t.Parallel()
        // ...
    })
}
```

---

## Anti-patterns

| Anti-pattern                         | Problem                          | Fix                                                 |
| ------------------------------------ | -------------------------------- | --------------------------------------------------- |
| Testing implementation, not behavior | Tests break on refactor          | Assert on outputs, not internal state               |
| Excessive mocking                    | Mocks pass while real code fails | Use interfaces + fakes; integration-test boundaries |
| No `t.Helper()` in helpers           | Error points to wrong line       | Add `t.Helper()` as first line                      |
| Benchmark without `b.ResetTimer()`   | Setup time skews results         | Call `b.ResetTimer()` after setup                   |

---

## Read On Demand

| Read When                                                                                                 | File                                               |
| --------------------------------------------------------------------------------------------------------- | -------------------------------------------------- |
| Full examples: table-driven, TestMain, parallel, golden files, httptest, testify, benchmarks, fuzz, pprof | [Testing Patterns](references/testing-patterns.md) |

---

## Benchmark

Scenario: `.benchmarks/scenarios/golang-testing-001-table-driven.md` · Run: 2026-08-31 · Log: `.benchmarks/runs/2026-08-31/golang-testing-001-table-driven.json`

| Model             | Without | With | Delta |
| ----------------- | ------- | ---- | ----- |
| claude-opus-4-8   | 100%    | 100% | +0%   |
| claude-sonnet-4-6 | 100%    | 100% | +0%   |
| claude-haiku-4-5  | 100%    | 100% | +0%   |

> **NEUTRAL (run 2026-08-31)**. All models 100% with and without — table-driven tests are default behavior at this task size. Gate per `.agents/skills/skill-optimizer/rules/release-gates.md`.

