Generated banner for Golang Testing
Generated logo for Golang Testing

Golang Testing

Samuel BertheCommunityCreated by samberDistributed by Samuel Berthe

  • Skill
  • Guidelines
  • Software development
  • MIT
  • No issues found

From the author

Production-ready Golang tests — table-driven tests, testify suites and mocks, parallel tests, fuzzing, fixtures, goroutine leak detection with goleak, snapshot testing, code coverage, integration tests, idiomatic test naming. Use when writing or reviewing Go tests, choosing a testing approach, setting up Go test CI, or debugging flaky/slow tests. For testify-specific APIs see `samber/cc-skills-golang@golang-stretchr-testify`; for measurement methodology see `samber/cc-skills-golang@golang-benchmark`.
— the description by samber

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

This skill provides a structured approach to Go testing, covering table-driven tests, integration testing, fuzzing, and goroutine leak detection. It emphasizes idiomatic practices such as naming conventions, file organization, and the use of subtests to ensure tests are maintainable and reliable. The skill includes strategies for auditing existing test suites, debugging flaky tests, and integrating testing tools into development workflows.

What it does

  • Provides patterns for table-driven tests and subtests
  • Outlines conventions for test file naming and organization
  • Guides the use of build tags for separating integration tests
  • Explains how to detect goroutine leaks using goleak
  • Offers strategies for auditing test suites for gaps and flakiness
  • Defines best practices for mocking and using testing helpers

When to use it

  • When writing new tests for Go source code
  • When reviewing Go test code in pull requests
  • When auditing an existing test suite for quality and coverage gaps
  • When debugging flaky or slow tests
  • When setting up test infrastructure for Go projects

Advantages

  • Promotes consistent and idiomatic testing patterns
  • Helps identify and prevent common testing pitfalls
  • Supports reliable test execution through parallelization and race detection
  • Facilitates better test maintenance through clear organization rules

Usage examples

“How should I structure a table-driven test in Go?”
The agent provides a template using a struct slice for test cases and t.Run for executing each case, ensuring proper naming and isolation.
“How do I identify goroutine leaks in my tests?”
The agent explains how to integrate go.uber.org/goleak into TestMain or individual tests to verify that no goroutines remain after execution.
“How should I organize my test files?”
The agent advises naming test files after the source file they test (e.g., foo.go to foo_test.go) and maintaining the same function order.
“How can I run only my integration tests?”
The agent instructs to use build tags with the command go test -tags=integration ./... to isolate integration tests from unit tests.

Install

  1. Download the skill as a ZIP:

    https://mp-staging.autogineer.com/agent-skills/samber/golang-testing/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip golang-testing.zip -d .agents/skills

Documentation by samber

Persona: You are a Go engineer who treats tests as executable specifications. You write tests to constrain behavior, not to hit coverage targets.

Thinking mode: Reason as thoroughly as possible for test strategy design and failure analysis — shallow reasoning misses edge cases and produces brittle tests that pass today but break tomorrow. On Claude Code, use ultrathink to trigger extended thinking explicitly.

Orchestration mode: Fan out the three sub-agents described in Audit mode (unit quality and coverage gaps, integration isolation, goroutine/race issues) for auditing a large test suite, and merge their findings into one gap report. On Claude Code, use ultracode to opt into multi-agent orchestration explicitly.

Modes:

  • Write mode — generating new tests for existing or new code. Work sequentially through the code under test; use gotests to scaffold table-driven tests, then enrich with edge cases and error paths.

  • Review mode — reviewing a PR's test changes. Focus on the diff: check coverage of new behaviour, assertion quality, table-driven structure, and absence of flakiness patterns. Sequential.

  • Audit mode — auditing an existing test suite for gaps, flakiness, or bad patterns (order-dependent tests, missing t.Parallel(), implementation-detail coupling). Launch up to 3 parallel sub-agents split by concern: (1) unit test quality and coverage gaps, (2) integration test isolation and build tags, (3) goroutine leaks and race conditions.

  • Debug mode — a test is failing or flaky. Work sequentially: reproduce reliably, isolate the failing assertion, trace the root cause in production code or test setup.

Community default. A company skill that explicitly supersedes samber/cc-skills-golang@golang-testing skill takes precedence.

Dependencies:

  • gotests: go install github.com/cweill/gotests/gotests@latest

Go Testing Best Practices

This skill guides the creation of production-ready tests for Go applications. Follow these principles to write maintainable, fast, and reliable tests.

Best Practices Summary

  1. Table-driven tests MUST use named subtests -- every test case needs a name field passed to t.Run

  2. Integration tests MUST use build tags (//go:build integration) to separate from unit tests

  3. Tests MUST NOT depend on execution order -- each test MUST be independently runnable

  4. Independent tests SHOULD use t.Parallel() when possible

  5. Tests MUST assert observable behavior and public API contracts, not implementation details -- a test coupled to internals turns every refactor into a test rewrite while proving nothing about the contract

  6. Packages with goroutines SHOULD use goleak.VerifyTestMain in TestMain to detect goroutine leaks

  7. Use testify as helpers, not a replacement for standard library

  8. Mock interfaces, not concrete types

  9. Keep unit tests fast (< 1ms), use build tags for integration tests

  10. Run tests with race detection in CI

  11. Include examples as executable documentation

  12. Test files MUST be named after the source file under test, not after the function or method being tested

  13. Test functions SHOULD appear in the same order as the functions/methods they test in the source file

Test Structure and Organization

File Conventions
// package_test.go - tests in same package (white-box, access unexported)
package mypackage

// mypackage_test.go - tests in test package (black-box, public API only)
package mypackage_test

Name the test file after the source file it tests, not after the function or method under test. Go's convention is one test file per source file (foo.go -> foo_test.go), because tools (go test, coverage reports, IDE "jump to test" navigation, gotests) and reviewers all resolve tests by source file, not by symbol. A source file usually declares several functions/methods; splitting its tests by symbol name scatters them across many files and breaks that file-to-file mapping.

// ✓ Good — one test file per source file
helloworld.go       -> helloworld_test.go   // contains TestHelloWorld, TestAbcd, TestXyz, ...

// ✗ Bad — test file named after the function/method instead of the source file
helloworld.go       -> abcd_test.go         // wrong: should be helloworld_test.go

Exception: very large source files MAY be split into multiple _test.go files by concern (e.g. foo_test.go + foo_edgecases_test.go), but each split file's name MUST still be derived from the source file name, never from an individual function name. Prefer keeping a single _test.go file per source file even when it grows large — splitting adds navigation overhead and is rarely worth it; reach for the exception only when a single file becomes genuinely unwieldy to browse or review.

Within a test file, order test functions to match the order their tested functions/methods appear in the source file. A reader (human or agent) scrolling foo.go alongside foo_test.go can then find the matching test by position instead of searching; drift between the two orderings compounds every time either file grows.

Naming Conventions
func TestAdd(t *testing.T) { ... }               // function test
func TestMyStruct_MyMethod(t *testing.T) { ... } // method test
func BenchmarkAdd(b *testing.B) { ... }          // benchmark
func ExampleAdd() { ... }                        // example
func FuzzAdd(f *testing.F) { ... }               // fuzz test

Table-Driven Tests

Table-driven tests are the idiomatic Go way to test multiple scenarios. Always name each test case.

func TestCalculatePrice(t *testing.T) {
    tests := []struct {
        name     string
        quantity int
        unitPrice float64
        expected  float64
    }{
        {
            name:      "single item",
            quantity:  1,
            unitPrice: 10.0,
            expected:  10.0,
        },
        {
            name:      "bulk discount - 100 items",
            quantity:  100,
            unitPrice: 10.0,
            expected:  900.0, // 10% discount
        },
        {
            name:      "zero quantity",
            quantity:  0,
            unitPrice: 10.0,
            expected:  0.0,
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got := CalculatePrice(tt.quantity, tt.unitPrice)
            if got != tt.expected {
                t.Errorf("CalculatePrice(%d, %.2f) = %.2f, want %.2f",
                    tt.quantity, tt.unitPrice, got, tt.expected)
            }
        })
    }
}

Common Pitfall: Assert Scope Leaking into Subtests

Never create a testify assert/require instance in the parent test function and reuse it inside t.Run closures. assert.New(t) captures the exact *testing.T it was built with, so if that t belongs to the parent, every failure raised inside the subtest gets attributed to the parent test in go test output — the failing subtest itself still reports --- PASS, silently hiding which case broke. This happens whether or not the subtest calls t.Parallel().

// WRONG -- `is` is bound to the parent's t
func TestCalculatePrice(t *testing.T) {
    is := assert.New(t)
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            is.Equal(tt.expected, CalculatePrice(tt.quantity, tt.unitPrice)) // misattributed on failure
        })
    }
}

// RIGHT -- each subtest builds its own instance from its own t
func TestCalculatePrice(t *testing.T) {
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            is := assert.New(t)
            is.Equal(tt.expected, CalculatePrice(tt.quantity, tt.unitPrice))
        })
    }
}

Verify with a deliberately-broken case: if go test -v -run TestName shows --- FAIL: TestName but every --- PASS: TestName/subtest_name line still says PASS, the assert scope is leaking.

Unit Tests

Unit tests should be fast (< 1ms), isolated (no external dependencies), and deterministic.

Testing HTTP Handlers

Use httptest for handler tests with table-driven patterns. See HTTP Testing for examples with request/response bodies, query parameters, headers, and status code assertions.

Goroutine Leak Detection with goleak

Use go.uber.org/goleak to detect leaking goroutines, especially for concurrent code:

import (
    "testing"
    "go.uber.org/goleak"
)

func TestMain(m *testing.M) {
    goleak.VerifyTestMain(m)
}

To exclude specific goroutine stacks (for known leaks or library goroutines):

func TestMain(m *testing.M) {
    goleak.VerifyTestMain(m,
        goleak.IgnoreCurrent(),
    )
}

Or per-test:

func TestWorkerPool(t *testing.T) {
    defer goleak.VerifyNone(t)
    // ... test code ...
}

testing/synctest for Deterministic Goroutine Testing

testing/synctest (Go 1.25+) provides deterministic tests for goroutines, timers, deadlines, and context cancellation. Time advances only when all goroutines are blocked, making ordering predictable.

When to use synctest instead of real time:

  • Testing concurrent code with time-based operations (time.Sleep, time.After, time.Ticker)

  • When race conditions need to be reproducible

  • When tests are flaky due to timing issues

import (
    "context"
    "testing"
    "testing/synctest"
    "time"
)

func TestContextTimeout(t *testing.T) {
    synctest.Test(t, func(t *testing.T) {
        const timeout = 5 * time.Second

        ctx, cancel := context.WithTimeout(t.Context(), timeout)
        defer cancel()

        time.Sleep(timeout - time.Nanosecond)
        synctest.Wait()
        if err := ctx.Err(); err != nil {
            t.Fatalf("before timeout: %v", err)
        }

        time.Sleep(time.Nanosecond)
        synctest.Wait()
        if err := ctx.Err(); err != context.DeadlineExceeded {
            t.Fatalf("after timeout: got %v, want DeadlineExceeded", err)
        }
    })
}

Use synctest.Test in Go 1.25+ and later. Do not use the old Go 1.24 experimental synctest.Run API in Go 1.25+ code. If a module explicitly targets Go 1.24 and opts into GOEXPERIMENT=synctest, use the old API only as a compatibility fallback.

Key differences in synctest:

  • time.Sleep advances synthetic time instantly when the goroutine blocks

  • time.After fires when synthetic time reaches the duration

  • All goroutines run to blocking points before time advances

  • Test execution is deterministic and repeatable

  • Go 1.27+ adds synctest.Sleep(d) as a direct helper to advance the bubble's fake clock, equivalent to time.Sleep(d) followed by synctest.Wait() but without needing a real goroutine to block on

Go 1.27+ also adds httptest.NewTestServer(), an in-memory fake-network variant of httptest.NewServer that composes with synctest — no real socket, so server tests can run inside a synctest.Test bubble instead of needing httptest.NewServer plus real timers.

Test Timeouts

For tests that may hang, use a timeout helper that panics with caller location. See Helpers.

Benchmarks

Write benchmarks as sub-benchmarks (b.Run per variant) so each variant gets its own name in the output — that name is what comparison tooling diffs. For Go 1.24+, use b.Loop() rather than a b.N loop.

→ See Benchmarks in a Test Suite for the code shape and size-parameterized examples.

→ See samber/cc-skills-golang@golang-benchmark skill for measurement methodology: benchstat, profiling from benchmarks, and CI regression detection.

Go 1.26+: test artifacts

When a test, benchmark, or fuzz target needs to persist files for inspection, use ArtifactDir() instead of ad-hoc paths or repo-local output.

func TestRenderGoldenArtifact(t *testing.T) {
    dir := t.ArtifactDir()

    out := filepath.Join(dir, "rendered.json")
    if err := os.WriteFile(out, renderedBytes, 0o644); err != nil {
        t.Fatal(err)
    }

    t.Logf("artifact written: %s", out)
}

Available on *testing.T, *testing.B, and *testing.F in Go 1.26+.

Go 1.27+: stdversion runs automatically

go test now invokes the stdversion vet check by default, flagging any use of an API newer than the module's go directive. A CI failure from this check means either the go directive needs bumping or the code needs to stop using the newer API — it is not a check to silence.

Parallel Tests

Use t.Parallel() to run tests concurrently:

func TestParallelOperations(t *testing.T) {
    tests := []struct {
        name string
        data []byte
    }{
        {"small data", make([]byte, 1024)},
        {"medium data", make([]byte, 1024*1024)},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            t.Parallel()
            is := assert.New(t)

            result := Process(tt.data)
            is.NotNil(result)
        })
    }
}

Fuzzing

Use fuzzing to find edge cases and bugs:

func FuzzReverse(f *testing.F) {
    f.Add("hello")
    f.Add("")
    f.Add("a")

    f.Fuzz(func(t *testing.T, input string) {
        reversed := Reverse(input)
        doubleReversed := Reverse(reversed)
        if input != doubleReversed {
            t.Errorf("Reverse(Reverse(%q)) = %q, want %q", input, doubleReversed, input)
        }
    })
}

Examples as Documentation

ExampleXxx functions are executable documentation: go test compares their stdout to the // Output: comment, so a drifting example fails the build instead of misleading readers.

→ See Examples as Documentation for naming rules, Unordered output, and placement.

Code Coverage

Generate a profile with go test -coverprofile=coverage.out ./..., then read the uncovered lines with go tool cover -html=coverage.out. Coverage locates untested paths; it does not measure assertion quality, so treat a percentage as a gap finder rather than a target.

→ See Code Coverage for coverage modes, -coverpkg, and reporting pitfalls.

Integration Tests

Use build tags to separate integration tests from unit tests:

//go:build integration

package mypackage

func TestDatabaseIntegration(t *testing.T) {
    db, err := sql.Open("postgres", os.Getenv("DATABASE_URL"))
    if err != nil {
        t.Fatal(err)
    }
    defer db.Close()

    // Test real database operations
}

Run integration tests separately:

go test -tags=integration ./...

For Docker Compose fixtures, SQL schemas, and integration test suites, see Integration Testing.

Mocking

Mock interfaces, not concrete types. Define interfaces where consumed, then create mock implementations.

For mock patterns, test fixtures, and time mocking, see Mocking.

Enforce with Linters

Many test best practices are enforced automatically by linters: thelper, paralleltest, testifylint. See the samber/cc-skills-golang@golang-lint skill for configuration and usage.

Cross-References

  • → See samber/cc-skills-golang@golang-stretchr-testify skill for detailed testify API (assert, require, mock, suite)

  • → See samber/cc-skills-golang@golang-database skill (testing.md) for database integration test patterns

  • → See samber/cc-skills-golang@golang-concurrency skill for goroutine leak detection with goleak

  • → See samber/cc-skills-golang@golang-continuous-integration skill for CI test configuration and GitHub Actions workflows

  • → See samber/cc-skills-golang@golang-lint skill for testifylint and paralleltest configuration

  • → See samber/cc-skills-golang@golang-continuous-integration skill for automated AI-driven code review in CI using these guidelines

Quick Reference

go test ./...                          # all tests
go test -run TestName ./...            # specific test by exact name
go test -run TestName/subtest ./...    # subtests within a test
go test -run 'Test(Add|Sub)' ./...     # multiple tests (regexp OR)
go test -run 'Test[A-Z]' ./...         # tests starting with capital letter
go test -run 'TestUser.*' ./...        # tests matching prefix
go test -run '.*Validation.*' ./...    # tests containing substring
go test -run TestName/. ./...          # all subtests of TestName
go test -run '/(unit|integration)' ./... # filter by subtest name
go test -race ./...                    # race detection
go test -cover ./...                   # coverage summary
go test -bench=. -benchmem ./...       # benchmarks
go test -fuzz=FuzzName ./...           # fuzzing
go test -tags=integration ./...        # integration tests

Files

evals/evals.json

[
  {
    "id": 1,
    "name": "goleak-goroutine-leak-detection",
    "description": "Tests use goleak for goroutine leak detection, not just task completion",
    "prompt": "Write tests for a `workerpool` package. The package has a `Pool` struct with `Start(numWorkers int)`, `Submit(task func())`, and `Stop()` methods. Start spawns goroutines, Submit enqueues work, Stop shuts down gracefully. Write comprehensive unit tests covering start, submit tasks, and stop.",
    "trap": "Model writes normal unit tests verifying task completion but omits goroutine leak detection — Stop() may appear to work while leaking goroutines",
    "assertions": [
      {
        "id": "1.1",
        "text": "Uses goleak (go.uber.org/goleak) — either goleak.VerifyTestMain in TestMain or goleak.VerifyNone per-test — to detect goroutine leaks from the worker pool"
      },
      {
        "id": "1.2",
        "text": "Has a TestMain function if using goleak.VerifyTestMain (the package-level approach)"
      },
      {
        "id": "1.3",
        "text": "Tests verify that Stop() properly cleans up goroutines (not just that tasks complete)"
      },
      {
        "id": "1.4",
        "text": "Imports go.uber.org/goleak"
      }
    ]
  },
  {
    "id": 2,
    "name": "integration-build-tag-not-testing-short",
    "description": "Integration tests use //go:build integration tag; testing.Short() is not an acceptable alternative",
    "prompt": "Our team disagrees on how to separate integration tests from unit tests in our Go project. A teammate proposes:\n\n```go\nfunc TestUserRepository_Create(t *testing.T) {\n    if testing.Short() {\n        t.Skip(\"skipping integration test\")\n    }\n    db := connectToPostgres(t)\n    // ... test ...\n}\n```\n\nThey argue: 'testing.Short() is the Go standard way — it's in the stdlib, you can configure it with -short, and every Go developer knows it. Build tags are extra complexity for no benefit.'\n\nHow should integration tests be separated? Is the teammate's approach correct? Write the correct implementation for a TestUserRepository_Create integration test.",
    "trap": "Model accepts testing.Short() as a valid approach because it's a stdlib feature and the teammate's argument sounds reasonable. The skill teaches: build tags are required because testing.Short() still compiles tests into the binary, requires a flag to skip, and leaks DB connection attempts into normal test runs.",
    "assertions": [
      {
        "id": "2.1",
        "text": "Rejects testing.Short() as the primary separation mechanism — does not accept the teammate's approach as correct"
      },
      {
        "id": "2.2",
        "text": "Uses `//go:build integration` build tag (at the file level, before the package declaration)"
      },
      {
        "id": "2.3",
        "text": "Explains why build tags are preferred: tests using testing.Short() still compile and attempt connections when running without -short, whereas build-tagged files are completely excluded from compilation"
      },
      {
        "id": "2.4",
        "text": "Includes the command to run integration tests: go test -tags=integration ./..."
      }
    ]
  },
  {
    "id": 3,
    "name": "parallel-subtests-pure-function",
    "description": "Pure function subtests call t.Parallel(); top-level test also parallel",
    "prompt": "Write table-driven tests for a pure function `Slugify(input string) string` that converts titles to URL-friendly slugs (lowercase, hyphens for spaces, strips special chars). Test at least 6 cases: normal title, unicode, multiple spaces, empty string, already-slugified input, and special characters only.",
    "trap": "Model omits t.Parallel() since the function is pure and 'already fast enough', missing parallelism opportunities for stateless tests",
    "assertions": [
      {
        "id": "3.1",
        "text": "Subtests call t.Parallel() — these are independent pure function tests with no shared mutable state"
      },
      {
        "id": "3.2",
        "text": "Top-level test function also calls t.Parallel()"
      },
      {
        "id": "3.3",
        "text": "Each test case has a descriptive `name` field used in t.Run"
      },
      {
        "id": "3.4",
        "text": "At least 6 test cases as requested"
      },
      {
        "id": "3.5",
        "text": "No shared mutable state between subtests (each subtest captures its own test case variable)"
      }
    ]
  },
  {
    "id": 4,
    "name": "fake-clock-injection-for-time-dependent-tests",
    "description": "Time-dependent code must accept a clock interface so tests can use clockwork.FakeClock; real time.Sleep is unacceptable",
    "prompt": "Here is an existing RateLimiter implementation:\n\n```go\ntype RateLimiter struct {\n    limit   int\n    window  time.Duration\n    count   int\n    resetAt time.Time\n}\n\nfunc NewRateLimiter(limit int, window time.Duration) *RateLimiter {\n    return &RateLimiter{\n        limit:   limit,\n        window:  window,\n        resetAt: time.Now().Add(window),\n    }\n}\n\nfunc (r *RateLimiter) Allow() bool {\n    now := time.Now()\n    if now.After(r.resetAt) {\n        r.count = 0\n        r.resetAt = now.Add(r.window)\n    }\n    if r.count >= r.limit {\n        return false\n    }\n    r.count++\n    return true\n}\n```\n\nWrite tests that verify:\n1. Allow() returns true while under the limit\n2. Allow() returns false when the limit is exceeded\n3. The counter resets after the time window expires\n\nThe tests must run in milliseconds, not seconds. You may modify the implementation if needed.",
    "trap": "Model uses time.Sleep(window + small margin) to test window expiration — the code uses time.Now() directly, making tests slow and flaky. The skill teaches to refactor the code to accept a clock interface (clockwork.Clock) and inject a FakeClock in tests.",
    "assertions": [
      {
        "id": "4.1",
        "text": "Modifies the RateLimiter to accept a clock interface (e.g., clockwork.Clock or a custom Now() func) rather than calling time.Now() directly"
      },
      {
        "id": "4.2",
        "text": "Uses clockwork.FakeClock (or equivalent) in tests to advance time without real sleeping — tests run in microseconds"
      },
      {
        "id": "4.3",
        "text": "Tests the window reset scenario by advancing the fake clock past the window duration (e.g., fakeClock.Advance(window + time.Millisecond))"
      },
      {
        "id": "4.4",
        "text": "No real-time time.Sleep in test code; use synctest.Test/synctest.Wait or a fake clock for deterministic synthetic time"
      }
    ]
  },
  {
    "id": 5,
    "name": "consumer-site-interface-mocking",
    "description": "Tests define interfaces at the consumer site and mock those, not concrete structs",
    "prompt": "Test a `NotificationService` struct that has a `NotifyUser(userID string) error` method. It depends on two concrete structs: `SMTPClient` (with `Send(to, subject, body string) error`) and `AuditLogger` (with `Log(event string) error`). NotifyUser looks up the user's email, sends an email via SMTPClient, and logs the event via AuditLogger. Write comprehensive tests for NotifyUser.",
    "trap": "Model embeds or wraps concrete SMTPClient/AuditLogger in mock structs, or creates test doubles that shadow the concrete types, instead of extracting consumer-site interfaces",
    "assertions": [
      {
        "id": "5.1",
        "text": "Defines interfaces for the dependencies (e.g., EmailSender, Logger) rather than using the concrete SMTPClient/AuditLogger structs directly in tests"
      },
      {
        "id": "5.2",
        "text": "Creates mock implementations of these interfaces (using testify/mock or manual mocks)"
      },
      {
        "id": "5.3",
        "text": "Does NOT embed or wrap the concrete SMTPClient/AuditLogger structs in mock objects"
      },
      {
        "id": "5.4",
        "text": "Uses dependency injection — NotificationService accepts interfaces, not concrete types"
      },
      {
        "id": "5.5",
        "text": "Tests verify both happy path (send succeeds) and error scenarios (send fails, log fails)"
      }
    ]
  },
  {
    "id": 6,
    "name": "test-observable-behavior-not-internals",
    "description": "Tests verify behavior via public API only, not by inspecting the internal map field",
    "prompt": "Test a `UserCache` struct with `Get(id string) (*User, bool)`, `Set(id string, user *User)`, and `Len() int` methods. The cache has an internal `data map[string]*User` field. Write tests that verify the cache stores and retrieves users correctly. Make sure to verify the internal map state is consistent after each operation.",
    "trap": "Model directly accesses the internal data map field to verify state, coupling tests to implementation details",
    "assertions": [
      {
        "id": "6.1",
        "text": "Tests observable behavior through Get/Set/Len public API only"
      },
      {
        "id": "6.2",
        "text": "Does NOT directly access or inspect the internal `data` map field"
      },
      {
        "id": "6.3",
        "text": "Does NOT use same-package (white-box) testing to examine cache internals"
      },
      {
        "id": "6.4",
        "text": "Tests cover cache hit, cache miss, overwrite, and Len() correctness"
      }
    ]
  },
  {
    "id": 7,
    "name": "external-test-package-resists-internal-access",
    "description": "Tests use package_test to black-box test; does not access unexported state even when a teammate suggests it",
    "prompt": "Write tests for a `tokenizer` package that exports `Tokenize(input string) []Token` and a `Token` struct with `Type` and `Value` fields. The package has an unexported `tokenCache map[string][]Token` field on the package level for memoization.\n\nA teammate says: 'We should verify the cache is being populated correctly — access tokenCache directly in the tests. It's in the same package so it's fine, and it makes the tests more reliable since we can confirm memoization is working.'\n\nWrite the tests. Should you follow the teammate's suggestion?",
    "trap": "Model follows the teammate's suggestion and uses package tokenizer (white-box) to access tokenCache directly, reasoning that it makes tests more reliable. The skill teaches to use package tokenizer_test and test only observable behavior — cache behavior is an implementation detail, not a contract.",
    "assertions": [
      {
        "id": "7.1",
        "text": "Rejects the teammate's suggestion — does not access tokenCache directly in tests"
      },
      {
        "id": "7.2",
        "text": "Uses `package tokenizer_test` (external test package) instead of `package tokenizer`"
      },
      {
        "id": "7.3",
        "text": "Explains why: cache behavior is an implementation detail — tests should verify the observable contract (same input → same output), not whether a specific optimization is in place"
      },
      {
        "id": "7.4",
        "text": "Tests focus on functional correctness: same input tokenizes identically on repeated calls, edge cases, different token types"
      }
    ]
  },
  {
    "id": 8,
    "name": "example-functions-as-documentation",
    "description": "Packages need Example functions with // Output: comments that serve as executable documentation in go doc",
    "prompt": "I've written a `currency` package with these functions:\n\n```go\nfunc Format(cents int64, code string) string  // Format(1234, \"USD\") → \"$12.34\"\nfunc Parse(s string) (int64, string, error)   // Parse(\"$12.34\") → 1234, \"USD\", nil\nfunc Convert(cents int64, from, to string, rate float64) int64\n```\n\nA colleague says: 'The functions are self-explanatory — names and signatures are clear enough. We don't need extra documentation. Just write unit tests with good coverage.'\n\nWrite comprehensive test coverage for this package. Should you follow the colleague's advice about documentation?",
    "trap": "Model follows the colleague's advice and writes only table-driven unit tests without Example functions — missing the executable documentation that shows real usage in `go doc` and on pkg.go.dev. The skill teaches that Example functions serve as both tests and documentation.",
    "assertions": [
      {
        "id": "8.1",
        "text": "Disagrees with the colleague — includes Example functions despite the advice to skip them"
      },
      {
        "id": "8.2",
        "text": "Includes at least one Example function (ExampleFormat, ExampleParse, or ExampleConvert)"
      },
      {
        "id": "8.3",
        "text": "Example functions have `// Output:` comments so they are verified by go test"
      },
      {
        "id": "8.4",
        "text": "Explains that Example functions serve as executable documentation visible in go doc and pkg.go.dev — not just tests"
      }
    ]
  },
  {
    "id": 9,
    "name": "fuzz-test-for-critical-functions",
    "description": "Security-critical functions get fuzz tests with seed corpus and property assertions",
    "prompt": "Write tests for a `SanitizeHTML(input string) string` function that strips all HTML tags from input while preserving text content. Make sure to test edge cases thoroughly — this function is critical for security.",
    "trap": "Model writes only table-driven tests for known edge cases, missing the fuzz test that would discover unexpected inputs causing XSS vulnerabilities",
    "assertions": [
      {
        "id": "9.1",
        "text": "Includes a fuzz test function (FuzzSanitizeHTML or similar)"
      },
      {
        "id": "9.2",
        "text": "Fuzz test uses f.Add() to provide seed corpus entries"
      },
      {
        "id": "9.3",
        "text": "Fuzz test includes property-based assertions (e.g., output contains no < or > characters, or double-sanitize is idempotent)"
      },
      {
        "id": "9.4",
        "text": "Also includes regular table-driven tests for known edge cases"
      },
      {
        "id": "9.5",
        "text": "Table tests cover tricky cases like nested tags, unclosed tags, or script tags"
      }
    ]
  },
  {
    "id": 10,
    "name": "test-helper-t-helper-attribution",
    "description": "Test helpers must call t.Helper() so failures point to the caller's line, not the helper's internal line",
    "prompt": "I have this test helper and some tests using it:\n\n```go\nfunc requireNoError(t *testing.T, err error, msg string) {\n    if err != nil {\n        t.Fatalf(\"%s: unexpected error: %v\", msg, err)\n    }\n}\n\nfunc TestProcessOrder(t *testing.T) {\n    order := NewOrder(\"prod-1\", 2)\n    err := order.Validate()\n    requireNoError(t, err, \"validate\")\n\n    err = order.Submit()\n    requireNoError(t, err, \"submit\")\n}\n```\n\nWhen Validate() fails, the test output reports a failure at the `t.Fatalf` line inside `requireNoError`, not at the `requireNoError(t, err, \"validate\")` call site in `TestProcessOrder`. Is this a problem? How do you fix it?",
    "trap": "Model says this is expected behavior or suggests switching to t.Error() instead of the real fix. The skill teaches that t.Helper() must be called as the first statement in the helper so Go's test framework reports failures at the caller's line.",
    "assertions": [
      {
        "id": "10.1",
        "text": "Identifies this as a real problem — the line number pointing to the helper's internal Fatalf is unhelpful for debugging which call caused the failure"
      },
      {
        "id": "10.2",
        "text": "Fixes it by adding t.Helper() as the first statement in requireNoError — not by restructuring the helper or using a different assertion method"
      },
      {
        "id": "10.3",
        "text": "Explains that t.Helper() marks the function as a test helper so that the testing framework reports the caller's file:line instead of the helper's file:line"
      },
      {
        "id": "10.4",
        "text": "Does NOT suggest switching to t.Error() as the fix — t.Helper() is the correct solution regardless of t.Fatal vs t.Error"
      }
    ]
  },
  {
    "id": 11,
    "name": "httptest-recorder-not-real-server",
    "description": "HTTP handler tests use httptest.NewRecorder, not a real HTTP server",
    "prompt": "Write end-to-end tests for a REST API handler `HandleCreateOrder(w http.ResponseWriter, r *http.Request)` that accepts POST with JSON body `{\"product\": \"...\", \"quantity\": N}`. It returns 201 with the order JSON on success, 400 for invalid JSON, and 422 for validation errors (empty product, quantity <= 0). Test it like a real client would call it.",
    "trap": "Model starts a real HTTP server with httptest.NewServer or net/http ListenAndServe, adding unnecessary network overhead and port allocation to tests",
    "assertions": [
      {
        "id": "11.1",
        "text": "Uses httptest.NewRecorder (not httptest.NewServer or a real HTTP server)"
      },
      {
        "id": "11.2",
        "text": "Table-driven with named test cases covering multiple scenarios"
      },
      {
        "id": "11.3",
        "text": "Tests at least 3 status codes (201, 400, 422)"
      },
      {
        "id": "11.4",
        "text": "Verifies response body content (not just status code)"
      },
      {
        "id": "11.5",
        "text": "Sets proper Content-Type header on requests"
      }
    ]
  },
  {
    "id": 12,
    "name": "testify-suite-for-integration",
    "description": "Integration tests use testify/suite with SetupSuite/TearDownTest for organized setup/teardown",
    "prompt": "Write integration tests for an `OrderRepository` that interacts with PostgreSQL. It has `Create(order *Order) error`, `GetByID(id string) (*Order, error)`, and `ListByUserID(userID string) ([]*Order, error)`. Tests need database setup (create tables), per-test data cleanup, and graceful teardown. Organize them cleanly so setup/teardown happens automatically. These must not run during normal unit tests.",
    "trap": "Model uses TestMain or plain setup functions with defer for teardown, mixing setup concerns into each test instead of a suite",
    "assertions": [
      {
        "id": "12.1",
        "text": "Uses testify/suite.Suite struct embedding for test organization"
      },
      {
        "id": "12.2",
        "text": "Has SetupSuite (or similar) for one-time database connection and schema setup"
      },
      {
        "id": "12.3",
        "text": "Has SetupTest or TearDownTest for per-test data cleanup (e.g., TRUNCATE)"
      },
      {
        "id": "12.4",
        "text": "Has TearDownSuite for graceful shutdown (close DB, docker-compose down)"
      },
      {
        "id": "12.5",
        "text": "Uses `//go:build integration` build tag"
      },
      {
        "id": "12.6",
        "text": "Has a runner function `func TestXxx(t *testing.T) { suite.Run(t, ...) }`"
      }
    ]
  },
  {
    "id": 13,
    "name": "benchmark-report-allocs-and-input-sizes",
    "description": "Benchmarks use b.ReportAllocs(), test multiple input sizes, and follow naming conventions",
    "prompt": "Write benchmarks for a `Compress(data []byte) ([]byte, error)` function that compresses byte slices. We need to measure performance to decide if this is fast enough for our hot path. Just write the benchmark tests.",
    "trap": "Model writes a single benchmark with one input size and omits b.ReportAllocs(), missing allocation tracking and size-scaling analysis",
    "assertions": [
      {
        "id": "13.1",
        "text": "Calls b.ReportAllocs() to track memory allocations per operation"
      },
      {
        "id": "13.2",
        "text": "Tests multiple input sizes using b.Run with descriptive sub-benchmark names (e.g., size=1KB, size=1MB)"
      },
      {
        "id": "13.3",
        "text": "Uses b.Loop() for Go 1.24+ benchmark loops; uses legacy b.N only for older module targets"
      },
      {
        "id": "13.4",
        "text": "Follows benchmark naming convention: BenchmarkCompress or BenchmarkCompress_<variant>"
      },
      {
        "id": "13.5",
        "text": "Prevents compiler optimization of the result (assigns to a package-level variable or uses _ =)"
      },
      {
        "id": "13.6",
        "text": "Does NOT include setup/allocation costs inside the timed loop (or uses b.ResetTimer if setup is needed)"
      }
    ]
  },
  {
    "id": 14,
    "name": "race-detection-and-test-independence",
    "description": "Tests for concurrent code include -race flag guidance and ensure test independence (no order dependence)",
    "prompt": "Write tests for a `SafeMap[K comparable, V any]` struct that provides a goroutine-safe map with `Get(key K) (V, bool)`, `Set(key K, value V)`, `Delete(key K)`, and `Len() int` methods. Multiple goroutines will call these concurrently. Write thorough tests including concurrent access scenarios. Also include a note on how to run these tests in CI.",
    "trap": "Model writes concurrent tests but omits -race flag guidance for CI and doesn't ensure tests are independently runnable (e.g., shares map state between test functions)",
    "assertions": [
      {
        "id": "14.1",
        "text": "Includes concurrent test scenarios where multiple goroutines call Get/Set/Delete simultaneously"
      },
      {
        "id": "14.2",
        "text": "Recommends running with -race flag (go test -race) for CI or includes it in a run command comment"
      },
      {
        "id": "14.3",
        "text": "Each test function creates its own SafeMap instance — no shared state between test functions"
      },
      {
        "id": "14.4",
        "text": "Uses sync.WaitGroup or similar synchronization to coordinate concurrent test goroutines"
      },
      {
        "id": "14.5",
        "text": "Tests are independently runnable (any single test can pass when run in isolation with -run)"
      }
    ]
  }
]

Requirements and compatibility

Written for: Claude Code, OpenAI Codex

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

Needs: Declares allowed tools, Claude Code frontmatter

The author says: Designed for Claude Code, Codex or similar harness, and for projects using Golang.

Security

No issues found by our static scan· No issues found

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

Created by
samber
Publisher
Samuel Berthe (Community)
Distributed by
Samuel Berthe
Source
samber/cc-skills-golang · skills/golang-testing · commit bb1d43d
Licence
MIT License (redistribution allowed)
  • LICENSE (added by AutoGineer from LICENSE)
  • ATTRIBUTION.md (written by AutoGineer)
Synced

AutoGineer is not affiliated with or endorsed by samber or Samuel Berthe.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download golang-testing.zip