Testland
Browse all skills & agents

go-test

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.

Install with skills.sh (any agent)

npx skills add testland/qa --skill go-test
View source

go-test

Overview

Per pkg.go.dev/testing (opens in new window):

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:

  • Stdlib: zero install; works wherever Go works.
  • Convention-driven: _test.go suffix; TestXxx / BenchmarkXxx / FuzzXxx / ExampleXxx function-name prefixes.
  • Table-driven idiom: built into the language style.
  • Built-in benchmarks + fuzzing (Go 1.18+ for fuzz).

Step 1 - First test

// 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

Step 2 - Table-driven tests (Go idiom)

Per pkg.go.dev/testing#hdr-Subtests_and_Sub_benchmarks (opens in new window):

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.

Step 3 - t.Parallel()

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()
        // ...
    })
}

Step 4 - Benchmarks

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

Step 5 - Examples (executable docs)

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.

Step 6 - Fuzzing (Go 1.18+)

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.

Step 7 - Coverage

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

Step 8 - Build tags for selective compilation

//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.

Step 9 - Test helpers

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).

Step 10 - CI integration

- 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 in qa-test-reporting):

go install github.com/jstemmer/go-junit-report/v2@latest
go test -v ./... | go-junit-report > junit.xml

Anti-patterns

Anti-patternWhy it failsFix
Skip t.Parallel() everywhereSlow test suite at scaleMark parallel-safe tests (Step 3)
Forget loop-variable capture pre-Go 1.22All subtests share last iteration's valuett := tt (Step 3)
Skip -race in CIRace conditions ship to prodAlways -race (Step 10)
Use assert from testify everywhereStdlib is sufficient + idiomaticPlain if got != want { t.Errorf }
Multiple t.Errorf in single sub-conditionFail-fast loses contextOne assertion per logical thing

Limitations

  • No assertion library in stdlib - community uses testify/stretchr/testify for richer matchers (and that's idiomatic in Go ecosystems).
  • No fixture concept - use t.Cleanup + helper functions.
  • No parametrize beyond table-driven loops.
  • No mocking library in stdlib - use interfaces + hand-written fakes (Go style) or gomock for codegen.

References

  • go-test-pkg (opens in new window) - testing package documentation
  • go-subtests (opens in new window) - subtests + sub-benchmarks
  • pkg.go.dev/cmd/go#hdr-Testing_flags - go test flags
  • pkg.go.dev/testing#hdr-Fuzzing - fuzz testing
  • github.com/jstemmer/go-junit-report - JUnit XML reporter
  • ginkgo-tests, cargo-test, rstest-tests - sister tools
  • test-code-conventions (qa-test-review)

Related skills

cargo-test

Configures and runs Rust's built-in `cargo test` - `#[test]` + `#[should_panic]` + `Result<(), E>` returns; integration tests in `tests/`; doc-tests embedded in `///` comments; `--lib` / `--bins` / `--all-targets` / `--workspace` selection; `cargo bench` (nightly) + Criterion (stable); `cargo test -- --test-threads=1` for serial; `cargo test -- --nocapture` to see println output. Use for any Rust project - testing is built into Cargo, no install needed.

ginkgo-tests

Configures and runs Ginkgo - Go BDD test framework with `Describe` / `Context` / `It` nesting; `BeforeEach` / `AfterEach` / `JustBeforeEach` / `JustAfterEach` lifecycle; Gomega matchers DSL (`Expect(actual).To(Equal(expected))`); parallel execution via `-p`; focus (`F` prefix) + skip (`P` prefix); `DescribeTable` + `Entry` for parametrized tests; `ginkgo` CLI tool. Use when working with Go on a BDD-style test suite (Kubernetes-ecosystem standard).

go-rust-mocking

Isolates dependencies in Go and Rust unit tests using test-double generation - Go: `go.uber.org/mock` (gomock + mockgen codegen) and `github.com/stretchr/testify/mock` (hand-written stubs); Rust: `mockall` crate with `#[automock]` for traits and `mock!` macro for structs and external traits. Use when a unit test reaches a database, HTTP client, file system, or any interface boundary that must be replaced with a controlled fake to keep tests fast, deterministic, and isolated.

rstest-tests

Configures and runs rstest - Rust parametrized + fixture-based testing crate; `#[rstest]` attribute + `#[case(...)]` for parametrize; `#[fixture]` for reusable test setup; matrix tests via multiple `#[case]` × N (cartesian product); async test support via `#[async_std::test]` / `#[tokio::test]` + `#[rstest]`; `#[future]` for async fixtures. Use when working with Rust and needing parametrize/fixture patterns beyond stdlib `#[test]`.