
Go Testing
- 981 installs
- 137 repo stars
- Updated June 20, 2026
- cxuu/golang-skills
go-testing is a Claude Code skill that generates and reviews idiomatic Go test code using table-driven tests, subtests, helpers, and cmp.Diff for developers who need reliable unit test coverage.
About
go-testing is a Go testing skill grounded in Google and Uber style guides that teaches table-driven tests, subtests, parallel execution, test helpers, test doubles, and assertions with github.com/google/go-cmp. The skill specifies when to use t.Error versus t.Fatal, patterns for subtests and parallel tests, and cmp.Diff for struct comparisons instead of brittle equality checks. Developers reach for go-testing when asked to write a test for a Go function, refactor flaky tests, or review test quality in packages and services. The skill explicitly excludes benchmark performance testing, which belongs in the separate go-performance skill.
- Applies to writing, reviewing, or improving Go test code including table-driven tests, subtests, parallel tests, test he
- Triggers automatically when a user asks to write a test for any Go function
- Enforces normative failure messages that include function name, inputs, actual vs expected without needing to read test
- Recommends t.Error vs t.Fatal, t.Helper(), t.Cleanup(), and cmp.Diff usage patterns
- Sources patterns from Google and Uber Go Style Guides
Go Testing by the numbers
- 981 all-time installs (skills.sh)
- +42 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #537 of 2,153 Testing & QA skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/cxuu/golang-skills --skill go-testingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 981 |
|---|---|
| repo stars | ★ 137 |
| Security audit | 3 / 3 scanners passed |
| Last updated | June 20, 2026 |
| Repository | cxuu/golang-skills ↗ |
How do you write idiomatic table-driven Go tests?
Generate, review, and improve high-quality Go test code using table-driven tests, subtests, helpers, and cmp.Diff assertions.
Who is it for?
Go developers writing or reviewing unit tests in services, CLIs, and libraries who want Google/Uber-style test patterns.
Skip if: Teams needing Go benchmark profiling or performance regression tests covered by the separate go-performance skill.
When should I use this skill?
A developer asks to write, review, or improve Go tests, including table-driven tests, subtests, or cmp.Diff comparisons.
What you get
Go _test.go files with table-driven cases, subtests, helpers, and cmp.Diff assertions.
- _test.go files
- table-driven test cases
- cmp.Diff assertions
Files
Go Testing
Compatibility: Diff examples may use github.com/google/go-cmp.Resource Routing
scripts/gen-table-test.sh- Run when generating a table-driven test scaffold.assets/table-test-template.go- Use as a copyable table-test starting point.references/TABLE-DRIVEN-TESTS.md- Read when choosing table tests, subtests, or parallel test patterns.references/TEST-HELPERS.md- Read when writing helpers, fixtures, cleanup, or test doubles.references/TEST-ORGANIZATION.md- Read when structuring packages, black-box tests, or larger test suites.references/VALIDATION-APIS.md- Read when choosingt.Error,t.Fatal,cmp.Diff, or assertion style.references/INTEGRATION.md- Read when testing external services, HTTP handlers, databases, or long-running setup.
Quick Reference
| Pattern | Use When |
|---|---|
t.Error | Default — report failure, keep running |
t.Fatal | Setup failed or continuing is meaningless |
cmp.Diff | Comparing structs, slices, maps, protos |
| Table-driven | Many cases share identical logic |
| Subtests | Need filtering, parallel execution, or naming |
t.Helper() | Any test helper function (call as first statement) |
t.Cleanup() | Teardown in helpers instead of defer |
---
Useful Test Failures
Normative: Test failures must be diagnosable without reading the test
source.
Every failure message must include: function name, inputs, actual (got), and expected (want). Use the format YourFunc(%v) = %v, want %v.
// Good:
t.Errorf("Add(2, 3) = %d, want %d", got, 5)
// Bad: Missing function name and inputs
t.Errorf("got %d, want %d", got, 5)Always print got before want: got %v, want %v — never reversed.
---
No Assertion Libraries
Normative: Do not use assertion libraries. Use cmp.Diff for complexcomparisons.
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("GetPost() mismatch (-want +got):\n%s", diff)
}For protocol buffers, add protocmp.Transform() as a cmp option. Always include the direction key (-want +got) in diff messages. Avoid comparing JSON/serialized output — compare semantically instead.
---
t.Error vs t.Fatal
Normative: Use t.Error by default to report all failures in one run.Use t.Fatal only when continuing is impossible.Choose `t.Fatal` when:
- Setup fails (DB connection, file load)
- The next assertion depends on the previous one succeeding (e.g., decode after
encode)
Never call `t.Fatal`/`t.FailNow` from a goroutine other than the test goroutine — use t.Error instead.
---
Table-Driven Tests
See assets/table-test-template.go when scaffolding a new table-driven test and need the canonical struct, loop, and subtest layout.Advisory: Use table-driven tests when many cases share identical logic.
Use table tests when: all cases run the same code path with no conditional setup, mocking, or assertions. A single shouldErr bool is acceptable.
Don't use table tests when: cases need complex setup, conditional mocking, or multiple branches — write separate test functions instead.
Key rules:
- Use field names when cases span many lines or have same-type adjacent fields
- Include inputs in failure messages — never identify rows by index
Validation: After generating or modifying tests, run go test -run TestXxx -v to verify the tests compile and pass. Fix any compilation errors before proceeding.---
Test Helpers
Normative: Test helpers must callt.Helper()first and uset.Cleanup()
for teardown.
func setupTestDB(t *testing.T) *sql.DB {
t.Helper()
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatalf("Could not open database: %v", err)
}
t.Cleanup(func() { db.Close() })
return db
}---
Test Error Semantics
Advisory: Test error semantics, not error message strings.
// Bad: Brittle string comparison
if err.Error() != "invalid input" { ... }
// Good: Semantic check
if !errors.Is(err, ErrInvalidInput) { ... }For simple presence checks when specific semantics don't matter:
if gotErr := err != nil; gotErr != tt.wantErr {
t.Errorf("f(%v) error = %v, want error presence = %t", tt.input, err, tt.wantErr)
}---
Related Skills
- Error testing: See go-error-handling when testing error semantics with
errors.Is/errors.Asor sentinel errors - Interface mocking: See go-interfaces when creating test doubles by implementing interfaces at the consumer side
- Naming test functions: See go-naming when naming test functions, subtests, or test helper utilities
- Linter integration: See go-linting when running linters alongside tests in CI or pre-commit hooks
package example_test
import "testing"
func TestExample(t *testing.T) {
tests := []struct {
name string
give string // TODO: replace with actual input type
want string // TODO: replace with actual output type
}{
{
name: "basic case",
give: "",
want: "",
},
// TODO: add more test cases
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Example(tt.give)
if got != tt.want {
t.Errorf("Example(%q) = %q, want %q", tt.give, got, tt.want)
}
// For richer diffs, consider:
// if diff := cmp.Diff(tt.want, got); diff != "" {
// t.Errorf("Example() mismatch (-want +got):\n%s", diff)
// }
})
}
}
Go Testing: Integration and Advanced Patterns
Detailed reference for TestMain, acceptance testing, and real transport testing. Sources: Google Go Style Guide (best-practices).
---
TestMain
Source: Google Go Style Guide (best-practices)
Use func TestMain(m *testing.M) when all tests in the package require common setup that needs teardown (e.g., a shared database). This should not be your first choice---prefer scoped test helpers or t.Cleanup when possible.
var db *sql.DB
func TestInsert(t *testing.T) { /* uses db */ }
func TestSelect(t *testing.T) { /* uses db */ }
func runMain(ctx context.Context, m *testing.M) (code int, err error) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
d, err := setupDatabase(ctx)
if err != nil {
return 0, err
}
defer d.Close()
db = d
return m.Run(), nil
}
func TestMain(m *testing.M) {
code, err := runMain(context.Background(), m)
if err != nil {
log.Fatal(err)
}
// defer statements do not run past os.Exit
os.Exit(code)
}Key points:
- Extract setup into a helper function (
runMain) sodeferworks correctly - Write failure messages to stderr via
log.Fatal - Ensure individual test cases remain hermetic---reset any global state they modify
---
Acceptance Testing
Source: Google Go Style Guide (best-practices)
Acceptance testing validates that an implementation upholds a contract, treating it as a black box. This pattern is useful when users implement your interfaces and you want to provide a reusable validation suite.
Structure
1. Create a test helper package (e.g., chesstest for package chess) 2. Export a validation function that accepts the implementation under test:
// Package chesstest provides acceptance tests for chess.Player implementations.
package chesstest
// ExercisePlayer tests a Player implementation in a single turn.
// Returns nil if the player makes a correct move, or an error describing
// the violation.
func ExercisePlayer(b *chess.Board, p chess.Player) error {
move := p.Move()
if putsOwnKingIntoCheck(b, move) {
return &IllegalMoveError{Move: move, Reason: "puts own king in check"}
}
return nil
}3. End users write simple tests against the validation function:
func TestAcceptance(t *testing.T) {
player := deepblue.New()
if err := chesstest.ExercisePlayer(chesstest.StartingBoard(), player); err != nil {
t.Errorf("Deep Blue player failed acceptance test: %v", err)
}
}Reserve t.Fatal for setup failures only---validation errors should be returned, not fataled.
---
Use Real Transports
Source: Google Go Style Guide (best-practices)
When testing component integrations over HTTP or RPC, prefer real transport round-trips over hand-implemented client mocks:
func TestAPIIntegration(t *testing.T) {
// Start a test server with a fake backend
srv := httptest.NewServer(newFakeHandler())
t.Cleanup(srv.Close)
// Use a real HTTP client against the test server
client := api.NewClient(srv.URL)
result, err := client.GetUser(t.Context(), "user-123")
if err != nil {
t.Fatalf("GetUser() error: %v", err)
}
if result.Name != "Test User" {
t.Errorf("GetUser().Name = %q, want %q", result.Name, "Test User")
}
}Using the production client with a test server ensures your test exercises as much real code as possible, avoiding the complexity of imitating client behavior. t.Context() requires Go 1.24 or newer; use an explicit context with cleanup-managed cancellation when maintaining older Go versions.
---
Common Mistakes
Calling os.Exit directly in TestMain
os.Exit terminates the process immediately — deferred cleanup functions never run. Extract setup/teardown into a helper so defer works correctly:
// Bad: defers won't run
func TestMain(m *testing.M) {
setup()
defer cleanup()
os.Exit(m.Run()) // cleanup() never executes
}
// Good: Extract to a helper function so defer runs before os.Exit
func runTests(m *testing.M) int {
setup()
defer cleanup()
return m.Run()
}
func TestMain(m *testing.M) {
os.Exit(runTests(m))
}Table-Driven Tests, Subtests, and Parallel Tests
Detailed reference for structuring table-driven tests and subtests in Go. Sources: Google Go Style Guide, Uber Go Style Guide.
---
Basic Structure
func TestCompare(t *testing.T) {
tests := []struct {
a, b string
want int
}{
{"", "", 0},
{"a", "", 1},
{"", "a", -1},
{"abc", "abc", 0},
}
for _, tt := range tests {
got := Compare(tt.a, tt.b)
if got != tt.want {
t.Errorf("Compare(%q, %q) = %v, want %v", tt.a, tt.b, got, tt.want)
}
}
}---
Best Practices
Use field names when test cases span many lines or have adjacent fields of the same type:
tests := []struct {
name string
input string
want int
}{
{name: "empty", input: "", want: 0},
{name: "single", input: "a", want: 1},
}Don't identify rows by index — include inputs in failure messages instead of Case #%d failed.
---
Avoid Complexity in Table Tests
When test cases need complex setup, conditional mocking, or multiple branches, prefer separate test functions over table tests.
// Bad: Too many conditional fields make tests hard to understand
tests := []struct {
give string
want string
wantErr error
shouldCallX bool
shouldCallY bool
giveXResponse string
giveXErr error
giveYResponse string
giveYErr error
}{...}
for _, tt := range tests {
t.Run(tt.give, func(t *testing.T) {
if tt.shouldCallX {
xMock.EXPECT().Call().Return(tt.giveXResponse, tt.giveXErr)
}
if tt.shouldCallY {
yMock.EXPECT().Call().Return(tt.giveYResponse, tt.giveYErr)
}
// ...
})
}
// Good: Separate focused tests are clearer
func TestShouldCallX(t *testing.T) {
xMock.EXPECT().Call().Return("XResponse", nil)
got, err := DoComplexThing("inputX", xMock, yMock)
// assert...
}
func TestShouldCallYAndFail(t *testing.T) {
yMock.EXPECT().Call().Return("YResponse", nil)
_, err := DoComplexThing("inputY", xMock, yMock)
// assert error...
}Table tests work best when:
- All cases run identical logic (no conditional assertions)
- Setup is the same for all cases
- No conditional mocking based on test case fields
- All table fields are used in all tests
A single shouldErr field for success/failure is acceptable if the test body is short and straightforward.
---
Subtests
Use t.Run for better organization, filtering, and parallel execution.
Subtest Names
- Use clear, concise names:
t.Run("empty_input", ...),t.Run("hu_to_en", ...) - Avoid wordy descriptions or slashes (slashes break test filtering)
- Subtests must be independent — no shared state or execution order dependencies
Table Tests with Subtests
func TestTranslate(t *testing.T) {
tests := []struct {
name, srcLang, dstLang, input, want string
}{
{"hu_en_basic", "hu", "en", "köszönöm", "thank you"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Translate(tt.srcLang, tt.dstLang, tt.input); got != tt.want {
t.Errorf("Translate(%q, %q, %q) = %q, want %q",
tt.srcLang, tt.dstLang, tt.input, got, tt.want)
}
})
}
}---
Parallel Tests
When using t.Parallel() in table tests, be aware of loop variable capture:
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
// Go 1.22+: tt is correctly captured per iteration
// Go 1.21-: add "tt := tt" here to capture the variable
got := Process(tt.give)
if got != tt.want {
t.Errorf("Process(%q) = %q, want %q", tt.give, got, tt.want)
}
})
}Test Helpers, Assertions, and Comparisons
Detailed reference for writing test helpers, avoiding assertion libraries, and choosing between t.Error and t.Fatal. Sources: Google Go Style Guide, Uber Go Style Guide.
---
Test Helper Pattern
Test helpers must call t.Helper() first so failures point to the caller. Use t.Fatal for setup failures, and t.Cleanup for teardown.
func mustLoadTestData(t *testing.T, filename string) []byte {
t.Helper()
data, err := os.ReadFile(filename)
if err != nil {
t.Fatalf("Setup failed: could not read %s: %v", filename, err)
}
return data
}
func setupTestDB(t *testing.T) *sql.DB {
t.Helper()
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatalf("Could not open database: %v", err)
}
t.Cleanup(func() { db.Close() })
return db
}Key rules:
- Call
t.Helper()as the first statement to attribute failures to the caller - Use
t.Fatalfor setup failures (don't return errors from helpers) - Use
t.Cleanup()for teardown instead of defer — it runs even if the test
calls t.FailNow
---
Avoiding Assertion Libraries
Normative: Do not create or use assertion libraries.
Assertion libraries fragment the developer experience and often produce unhelpful failure messages.
// Bad:
assert.IsNotNil(t, "obj", obj)
assert.StringEq(t, "obj.Type", obj.Type, "blogPost")
assert.IntEq(t, "obj.Comments", obj.Comments, 2)
// Good: Use cmp package and standard comparisons
want := BlogPost{
Type: "blogPost",
Comments: 2,
Body: "Hello, world!",
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("GetPost() mismatch (-want +got):\n%s", diff)
}Domain-Specific Comparisons
For domain-specific comparisons, return values or errors instead of calling t.Error:
func postLength(p BlogPost) int { return len(p.Body) }
func TestBlogPost(t *testing.T) {
post := BlogPost{Body: "Hello"}
if got, want := postLength(post), 5; got != want {
t.Errorf("postLength(post) = %v, want %v", got, want)
}
}---
Comparisons and Diffs
Prefer cmp.Equal and cmp.Diff for complex types. Always include the direction key (-want +got) in diff messages.
// Struct comparison
want := &Doc{Type: "blogPost", Authors: []string{"isaac", "albert"}}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("AddPost() mismatch (-want +got):\n%s", diff)
}
// Protocol buffers
if diff := cmp.Diff(want, got, protocmp.Transform()); diff != "" {
t.Errorf("Foo() mismatch (-want +got):\n%s", diff)
}Avoid unstable comparisons — don't compare JSON/serialized output that may change. Compare semantically instead.
---
t.Error vs t.Fatal: Detailed Guidance
Use t.Error to keep tests going and report all failures in a single run:
// Good: Report all mismatches
if diff := cmp.Diff(wantMean, gotMean); diff != "" {
t.Errorf("Mean mismatch (-want +got):\n%s", diff)
}
if diff := cmp.Diff(wantVariance, gotVariance); diff != "" {
t.Errorf("Variance mismatch (-want +got):\n%s", diff)
}Use t.Fatal when subsequent checks would be meaningless:
gotEncoded := Encode(input)
if gotEncoded != wantEncoded {
t.Fatalf("Encode(%q) = %q, want %q", input, gotEncoded, wantEncoded)
}
gotDecoded, err := Decode(gotEncoded)
if err != nil {
t.Fatalf("Decode(%q) error: %v", gotEncoded, err)
}Don't Call t.Fatal from Goroutines
Normative: Never callt.Fatal,t.Fatalf, ort.FailNowfrom a
goroutine other than the test goroutine. Use t.Error instead and let thegoroutine return naturally.
Test Organization Reference
Sources: Google Go Style Guide (best-practices, decisions).
---
Test Double Types
| Double | Purpose | State? | Verifies calls? |
|---|---|---|---|
| Stub | Returns canned data | No | No |
| Fake | Working but simplified implementation | Yes | No |
| Spy | Records calls for later inspection | Yes | Yes |
Prefer fakes over mocks. Fakes are more readable and don't require mock frameworks. Reserve spies for verifying side effects (e.g., an analytics event).
// Fake: Working in-memory implementation
type FakeUserStore struct {
users map[string]*User
}
func (f *FakeUserStore) GetUser(id string) (*User, error) {
u, ok := f.users[id]
if !ok {
return nil, ErrNotFound
}
return u, nil
}
// Spy: Records calls for later assertion
type SpyEmailSender struct{ Sent []string }
func (s *SpyEmailSender) Send(to, body string) error {
s.Sent = append(s.Sent, to)
return nil
}---
Test Double Naming Conventions
Advisory: Follow consistent naming for test doubles (stubs, fakes, spies).
Package naming: Create a *test package alongside production code (e.g., creditcardtest for package creditcard, fakeauthservice for a standalone fake service).
// Good: In package creditcardtest
// Single double — use simple name
type Stub struct{}
func (Stub) Charge(*creditcard.Card, money.Money) error { return nil }
// Multiple behaviors — name by behavior
type AlwaysCharges struct{}
type AlwaysDeclines struct{}
// Multiple types — include type name
type StubService struct{}
type StubStoredValue struct{}Local variables: Prefix test double variables with the double type for clarity at the call site:
// Good: Double type is immediately visible
spyCC := &creditcardtest.Spy{}
stubDB := &dbtest.Stub{Balance: 100}
// Bad: Ambiguous — is this real or a double?
cc := &creditcardtest.Spy{}
db := &dbtest.Stub{Balance: 100}---
Standalone Test Helper Packages
Create a standalone test helper package when multiple packages need the same double, the helper has enough logic to warrant its own tests, or you want to provide an acceptance test suite for interface implementers.
| Pattern | When to use | Example |
|---|---|---|
footest | General test helpers for package foo | creditcardtest, usertest |
fakeX | Standalone fake service package | fakeauthservice, fakestorage |
package usertest
func NewFakeStore(t *testing.T, users ...*user.User) *FakeUserStore {
t.Helper()
store := &FakeUserStore{users: make(map[string]*user.User)}
for _, u := range users {
store.users[u.ID] = u
}
return store
}Export constructors that accept *testing.T so they can call t.Helper() and t.Cleanup().
---
Test Packages
| Package Declaration | Use Case |
|---|---|
package foo | Same-package tests, can access unexported identifiers |
package foo_test | Black-box tests, avoids circular dependencies |
Both go in foo_test.go files in the same directory.
Use `package foo` (white-box) when you need to test unexported functions or internal state.
Use `package foo_test` (black-box) when testing only the public API, breaking import cycles, or verifying external usability.
package parser_test // Black-box: only tests exported API
import "mymodule/parser"
func TestParse(t *testing.T) {
got, err := parser.Parse("input")
// ...
}If a black-box test needs an unexported symbol, create export_test.go in package foo (not foo_test) that exposes it. Use this sparingly.
---
Setup Scoping
Advisory: Keep setup scoped to tests that need it.
Explicit setup in each test is clearer and avoids penalizing unrelated tests:
// Good: Explicit setup in tests that need it
func TestParseData(t *testing.T) {
data := mustLoadDataset(t)
// ...
}
func TestUnrelated(t *testing.T) {
// Doesn't pay for dataset loading
}Avoid global `init` for test setup — it runs for every test in the file, even unrelated ones.
Subtest setup: Use a parent test with t.Run when a group of subtests shares setup:
func TestDatabase(t *testing.T) {
db := setupTestDB(t)
t.Run("Insert", func(t *testing.T) {
// uses db
})
t.Run("Select", func(t *testing.T) {
// uses db
})
}This scopes the database lifecycle to the subtests that need it. Use TestMain only as a last resort (see INTEGRATION.md).
Extensible Validation APIs
Detailed reference for designing reusable test validation functions that callers can use for acceptance testing. Sources: Google Go Style Guide (best-practices).
---
The *test Package Export Pattern
When you own an interface that others implement, export a validation function in a companion *test package. This lets implementers verify correctness without duplicating your test logic.
// Package storagetest provides acceptance tests for storage.Backend.
package storagetest
// Verify runs a validation suite against any storage.Backend.
// Returns an error describing the first violation, or nil on success.
func Verify(b storage.Backend) error {
if err := verifyRoundTrip(b); err != nil {
return fmt.Errorf("round-trip: %w", err)
}
if err := verifyNotFound(b); err != nil {
return fmt.Errorf("not-found: %w", err)
}
return nil
}Callers write a thin test that plugs in their implementation:
func TestMyBackend(t *testing.T) {
b := mybackend.New(t)
if err := storagetest.Verify(b); err != nil {
t.Errorf("MyBackend failed acceptance: %v", err)
}
}---
Designing Extensible Validation Functions
*Return errors, not `testing.T failures.** This keeps validation functions usable as plain Go functions — callers decide whether a violation is t.Error or t.Fatal`.
// Good: Returns error — caller controls test flow
func ExercisePlayer(b *chess.Board, p chess.Player) error {
move := p.Move()
if putsOwnKingIntoCheck(b, move) {
return &IllegalMoveError{Move: move, Reason: "puts own king in check"}
}
return nil
}
// Bad: Calls t.Fatal — caller loses control
func ExercisePlayer(t *testing.T, b *chess.Board, p chess.Player) {
t.Helper()
move := p.Move()
if putsOwnKingIntoCheck(b, move) {
t.Fatalf("illegal move: %v puts own king in check", move)
}
}Use custom error types for rich diagnostics when needed:
type IllegalMoveError struct {
Move chess.Move
Reason string
}
func (e *IllegalMoveError) Error() string {
return fmt.Sprintf("illegal move %v: %s", e.Move, e.Reason)
}---
When to Use Validation APIs vs Simple Helpers
| Situation | Use |
|---|---|
| Interface you own, others implement | Validation API in *test package |
| Shared setup across tests in one package | Test helper with t.Helper() |
| Complex assertion reused in 2-3 tests | Helper returning error or bool |
| One-off setup or comparison | Inline test code |
Validation APIs are worth the extra package when:
- Multiple external packages will implement your interface
- The contract has non-obvious invariants that are easy to get wrong
- You want a single source of truth for "correct behavior"
Simple helpers are better when:
- The helper is a straightforward setup or comparison function
- The reuse is incidental, not part of a published contract
---
Naming Conventions
Name the function with a verb that signals scope: Verify, Exercise, RunConformance. Accept the interface under test as a parameter — never construct the implementation inside the validation package.
| Package | Function | Purpose |
|---|---|---|
storagetest | Verify | Validates a storage.Backend |
chesstest | ExercisePlayer | Validates a chess.Player |
cachetest | RunConformance | Full conformance suite for cache.Cache |
#!/usr/bin/env bash
set -euo pipefail
VERSION="1.0.0"
SCRIPT_NAME="$(basename "$0")"
usage() {
cat <<EOF
$SCRIPT_NAME v$VERSION — Generate a table-driven test scaffold for a Go function
USAGE
bash $SCRIPT_NAME [options] <FuncName> <package>
DESCRIPTION
Outputs a table-driven test file for the given function and package.
By default writes to stdout; use --output to write to a file.
Exits 0 on success, 2 on error.
OPTIONS
-h, --help Show this help message
-v, --version Show version
--output FILE Write to FILE instead of stdout
--force Allow --output to overwrite an existing file
--parallel Include t.Parallel() in generated test
--json Output structured JSON metadata to stdout
ARGUMENTS
FuncName Name of the function to test (must be exported/uppercase)
package Go package name for the test file
EXAMPLES
bash $SCRIPT_NAME ParseConfig config
bash $SCRIPT_NAME --parallel ParseConfig config
bash $SCRIPT_NAME --output config/parse_config_test.go ParseConfig config
bash $SCRIPT_NAME --force --output config/parse_config_test.go ParseConfig config
bash $SCRIPT_NAME --json --output config/parse_config_test.go ParseConfig config
bash $SCRIPT_NAME ParseConfig config > config/parse_config_test.go
EOF
}
json_escape() {
local s="$1"
s="${s//\\/\\\\}"
s="${s//\"/\\\"}"
s="${s//$'\t'/\\t}"
s="${s//$'\r'/}"
s="${s//$'\n'/\\n}"
printf '%s' "$s"
}
OUTPUT=""
PARALLEL=false
JSON_OUTPUT=false
FORCE=false
POSITIONAL=()
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help) usage; exit 0 ;;
-v|--version) echo "$SCRIPT_NAME v$VERSION"; exit 0 ;;
--output) OUTPUT="${2:?error: --output requires a file path}"; shift 2 ;;
--force) FORCE=true; shift ;;
--parallel) PARALLEL=true; shift ;;
--json) JSON_OUTPUT=true; shift ;;
-*) echo "error: unknown option: $1" >&2; usage >&2; exit 2 ;;
*) POSITIONAL+=("$1"); shift ;;
esac
done
if [[ ${#POSITIONAL[@]} -lt 2 ]]; then
echo "error: FuncName and package are required" >&2
usage >&2
exit 2
fi
FUNC="${POSITIONAL[0]}"
PKG="${POSITIONAL[1]}"
if [[ ! "$FUNC" =~ ^[A-Z][A-Za-z0-9_]*$ ]]; then
echo "error: FuncName '$FUNC' must be an exported Go identifier" >&2
exit 2
fi
if [[ ! "$PKG" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]]; then
echo "error: package '$PKG' must be a valid Go package identifier" >&2
exit 2
fi
case "$PKG" in
_|break|default|func|interface|select|case|defer|go|map|struct|chan|else|goto|package|switch|const|fallthrough|if|range|type|continue|for|import|return|var)
echo "error: package '$PKG' must not be a Go keyword or blank identifier" >&2
exit 2
;;
esac
generate_test() {
local parallel_top="" parallel_sub=""
if $PARALLEL; then
parallel_top=$'\tt.Parallel()\n'
parallel_sub=$'\t\t\tt.Parallel()\n'
fi
cat <<EOF
package ${PKG}
import (
"testing"
)
func Test${FUNC}(t *testing.T) {
${parallel_top} tests := []struct {
name string
give string // TODO: replace with actual input type
want string // TODO: replace with actual output type
}{
{
name: "basic case",
give: "",
want: "",
},
// TODO: add more test cases
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
${parallel_sub} got := ${FUNC}(tt.give)
if got != tt.want {
t.Errorf("${FUNC}(%q) = %q, want %q", tt.give, got, tt.want)
}
// For richer diffs, consider:
// if diff := cmp.Diff(tt.want, got); diff != "" {
// t.Errorf("${FUNC}() mismatch (-want +got):\n%s", diff)
// }
})
}
}
EOF
}
if [[ -n "$OUTPUT" ]]; then
OUTPUT_DIR="$(dirname "$OUTPUT")"
if [[ ! -d "$OUTPUT_DIR" ]]; then
echo "error: directory '$OUTPUT_DIR' does not exist" >&2
exit 2
fi
if [[ -f "$OUTPUT" ]] && ! $FORCE; then
echo "error: '$OUTPUT' already exists (use --force to overwrite)" >&2
exit 2
fi
generate_test > "$OUTPUT"
if $JSON_OUTPUT; then
FUNC_ESC="$(json_escape "$FUNC")"
PKG_ESC="$(json_escape "$PKG")"
OUTPUT_ESC="$(json_escape "$OUTPUT")"
cat <<EOF
{"func":"$FUNC_ESC","package":"$PKG_ESC","output_file":"$OUTPUT_ESC","parallel":$PARALLEL,"written":true}
EOF
else
echo "Wrote test scaffold to $OUTPUT"
fi
else
if $JSON_OUTPUT; then
generate_test >&2
FUNC_ESC="$(json_escape "$FUNC")"
PKG_ESC="$(json_escape "$PKG")"
cat <<EOF
{"func":"$FUNC_ESC","package":"$PKG_ESC","output_file":"","parallel":$PARALLEL,"written":false}
EOF
else
generate_test
fi
fi
exit 0
Related skills
How it compares
Pick go-testing over generic test-generation skills when the codebase is Go and you need idiomatic table-driven patterns rather than framework-agnostic examples.
FAQ
Does go-testing cover Go benchmark tests?
go-testing does not cover benchmark performance testing. The skill readme directs benchmark work to the separate go-performance skill and focuses on unit tests, table-driven cases, subtests, and cmp.Diff assertions.
Which assertion library does go-testing recommend?
go-testing recommends github.com/google/go-cmp for cmp.Diff struct comparisons. The skill is compatible with the go-cmp module and follows Google and Uber style guide testing patterns.
Is Go Testing safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.