
go-table-driven-tests
by Xe
Various tools, trinkets and experiments
SKILL.md
name: go-table-driven-tests description: 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.
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
Score
Total Score
Based on repository quality metrics
SKILL.mdファイルが含まれている
ライセンスが設定されている
100文字以上の説明がある
GitHub Stars 100以上
1ヶ月以内に更新
10回以上フォークされている
オープンIssueが50未満
プログラミング言語が設定されている
1つ以上のタグが設定されている
Reviews
Reviews coming soon



