Implements table-driven testing patterns in Go to reduce code duplication and improve test coverage for complex logic.
Install
mkdir -p .claude/skills/go-table-driven-tests && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/3238" && unzip -o skill.zip -d .claude/skills/go-table-driven-tests && rm skill.zipInstalls to .claude/skills/go-table-driven-tests
Activation
This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.
Write Go table-driven tests following Go community best practices and this repository's conventions. Use when writing or refactoring Go tests, especially when you notice repeated test patterns or copy-pasted test code.Key capabilities
- →Define test cases using slice or map patterns
- →Execute subtests using t.Run
- →Implement optional precondition setup functions
- →Run tests in parallel with t.Parallel
- →Compare errors using errors.Is
How it works
It iterates over a defined table of test cases, executing each as a subtest using t.Run to ensure systematic coverage and maintainable code.
Inputs & outputs
When to use go-table-driven-tests
- →Writing repeatable tests for functions with multiple inputs
- →Refactoring legacy test code to reduce duplication
- →Adding edge-case coverage to existing Go functions
About this skill
Go Table-Driven Tests
Overview
Table-driven tests are a Go testing idiom that reduces code duplication and makes tests more maintainable. Instead of writing separate test functions for each case, you define a table of test cases and iterate over it.
When to Use Table-Driven Tests
Use table-driven tests when:
- You find yourself copying and pasting test code
- You're testing the same function/behavior with multiple inputs
- You want to add more test cases without writing more test functions
- Edge cases and boundary conditions need systematic coverage
Do NOT use for: Completely unrelated test scenarios, or when each test requires substantially different setup/teardown logic.
Basic Template (Slice Pattern)
This is the most common pattern in this codebase:
func TestFunctionName(t *testing.T) {
cases := []struct {
name string
input string
want string
err error
}{
{
name: "simple case",
input: "a/b/c",
want: "a,b,c",
},
{
name: "empty input",
input: "",
want: "",
},
{
name: "invalid input",
input: "!!!",
want: "",
err: ErrInvalid,
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
got, err := FunctionName(tt.input)
if !errors.Is(err, tt.err) {
t.Errorf("FunctionName(%q) error = %v, want %v", tt.input, err, tt.err)
}
if got != tt.want {
t.Errorf("FunctionName(%q) = %q, want %q", tt.input, got, tt.want)
}
})
}
}
Map Pattern (For Non-Deterministic Test Ordering)
Use a map when you want to ensure test independence:
func TestFunctionName(t *testing.T) {
tests := map[string]struct {
input string
want string
}{
"simple case": {input: "a/b/c", want: "a,b,c"},
"empty input": {input: "", want: ""},
"trailing sep": {input: "a/b/c/", want: "a,b,c"},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
got := FunctionName(tc.input)
if got != tc.want {
t.Fatalf("%s: expected %q, got %q", name, tc.want, got)
}
})
}
}
Repository-Specific Conventions
Variable Naming
This codebase consistently uses these variable names:
| Purpose | Variable Name | Example |
|---|---|---|
| Test cases slice | cases, tt, cs | cases := []struct{...} |
| Loop variable | tt, cs, tc | for _, tt := range cases |
| Input field | input, in, inp | input: "test" |
| Expected output | want, expected | want: "result" |
| Actual output | got, output | got := Function() |
| Error field | err, wantErr | err: ErrInvalid |
| Name field | name | name: "descriptive name" |
Struct Field Guidelines
// Always use named fields (not anonymous structs in this codebase)
cases := []struct {
name string // Descriptive test name (REQUIRED when using slice pattern)
input Type // Input to function under test
want Type // Expected output
err error // Expected error (use errors.Is for comparison)
wantErr bool // Alternative: true if error is expected
precondition func(*testing.T) // Optional setup function
}{ ... }
Error Reporting
This repository uses these patterns:
// For error checking (preferred)
if !errors.Is(err, tt.err) {
t.Errorf("error = %v, want %v", err, tt.err)
}
// For simple comparisons
if got != tt.want {
t.Errorf("got %q, want %q", got, tt.want)
}
// Use t.Fatalf only when continuing doesn't make sense
// Use t.Errorf to see all test failures before stopping
Advanced Patterns
With Precondition Functions
When tests need specific setup:
for _, tt := range []struct {
name string
precondition func(*testing.T)
input string
err error
}{
{
name: "with listener",
precondition: func(t *testing.T) {
ln, err := net.Listen("tcp", ":8081")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { ln.Close() })
},
input: "test",
err: nil,
},
} {
t.Run(tt.name, func(t *testing.T) {
if tt.precondition != nil {
tt.precondition(t)
}
// test logic
})
}
Parallel Tests
For independent tests that can run in parallel:
func TestFunctionName(t *testing.T) {
cases := []struct {
name string
input string
want string
}{
// ... test cases
}
for _, tt := range cases {
tt := tt // Capture range variable (Go < 1.22)
t.Run(tt.name, func(t *testing.T) {
t.Parallel() // Marks this subtest as parallel
got := FunctionName(tt.input)
if got != tt.want {
t.Errorf("got %q, want %q", got, tt.want)
}
})
}
}
Best Practices
- Always use
t.Run()for subtests - This is 100% consistent in this codebase - Use descriptive test names - The
namefield should clearly describe what is being tested - Test one thing per case - Each table entry should test one specific behavior
- Include edge cases - Empty strings, nil values, maximum values, etc.
- Use
errors.Isfor error comparison - Not==orreflect.DeepEqual - Prefer
t.Errorfovert.Fatalf- See all failures before stopping - Keep test data inline - External files only for large golden test sets
Common Pitfalls to Avoid
- Forgetting
t.Run()- Without subtests, all failures appear at the same line - Using
Fatalfimmediately - You won't see other test failures - Not capturing range variable - In Go < 1.22, add
tt := ttbeforet.Run - Anonymous structs - This codebase prefers named structs for clarity
- Inconsistent naming - Stick to the conventions (
cases,tt,want,got)
Comparison with Traditional Tests
| Traditional | Table-Driven |
|---|---|
func TestFoo(t *testing.T) { ... } | func TestFoo(t *testing.T) { cases := []struct{...}{...} } |
| One test function per case | Single function, many cases |
| Hard to add new cases | Just add a row to the table |
| Verbose boilerplate | Concise, DRY code |
go test -run TestFoo_SpecificCase | go test -run TestFoo/name |
Running Specific Tests
# Run all tests in a function
go test -run TestFunctionName
# Run a specific subtest
go test -run TestFunctionName/descriptive_name
# Run all tests matching a pattern
go test -run TestFunctionName/.*/empty
# Verbose mode (see all test output)
go test -v
# Run with race detector
go test -race
References
When not to use it
- →Unrelated test scenarios
- →Tests requiring substantially different setup or teardown logic
Limitations
- →Requires named structs for clarity
- →Requires manual range variable capture for Go versions below 1.22
How it compares
Unlike traditional tests that require separate functions for each case, this approach uses a single function to iterate over a data-driven table.
Compared to similar skills
go-table-driven-tests side by side with the closest alternatives in the catalog.
| Skill | Installs | Updated | Safety | Difficulty |
|---|---|---|---|---|
| go-table-driven-tests (this skill) | 1 | 6mo | Review | Beginner |
| go-dev-guidelines | 14 | 9mo | No flags | Intermediate |
| code-formatting | 1 | 2mo | No flags | Beginner |
| tidb-test-guidelines | 2 | 28d | No flags | Intermediate |
Try saying
Example prompts that trigger this skill in your AI assistant.
More by Xe
View all by Xe →You might also like
go-dev-guidelines
jumppad-labs
This skill should be used when writing, refactoring, or testing Go code. It provides idiomatic Go development patterns, TDD-based workflows, project structure conventions, and testing best practices using testify/require and mockery. Activate this skill when creating new Go features, services, packages, tests, or when setting up new Go projects.
code-formatting
openshift
MANDATORY: When writing Go tests, you MUST use 'When...it should...' format for ALL test names. When writing any Go code, you MUST remind user to run 'make lint-fix' and 'make verify'. These are non-negotiable HyperShift requirements.
tidb-test-guidelines
pingcap
Decide where to place TiDB tests and how to write them (basic structure, naming, testdata usage). Use when asked about test locations, writing conventions, shard_count limits, casetest categorization, or when reviewing test changes in code review.
run-tests
pgschema
Run pgschema automated tests (go test) to validate diff logic, plan generation, and dump functionality using test fixtures
lsp-hover-testing
MadAppGang
Automated LSP hover validation for Dingo transpiler. Use when testing hover functionality, validating position mappings, checking for hover drift, or debugging LSP issues after sourcemap changes.
go-rig
mudrii
Use this skill when building, reviewing, or refactoring Go code in this repository. It adds process discipline for TDD/ATDD, package boundaries, dependency injection, and review quality, and complements the always-on rules in AGENTS.md.