go-table-driven-tests by Xe
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.
Content & Writing
310 Stars
17 Forks
Updated Jan 16, 2026, 10:08 AM
Why Use This
This skill provides specialized capabilities for Xe's codebase.
Use Cases
- Developing new features in the Xe repository
- Refactoring existing code to follow Xe standards
- Understanding and working with Xe's codebase structure
Install Guide
2 steps- 1
Skip this step if Ananke is already installed.
- 2
Skill Snapshot
Auto scan of skill assets. Informational only.
Valid SKILL.md
Checks against SKILL.md specification
Source & Community
Skill Stats
SKILL.md 257 Lines
Total Files 1
Total Size 0 B
License NOASSERTION
---
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:
```go
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:
```go
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
```go
// 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:
```go
// 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:
```go
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:
```go
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
1. **Always use `t.Run()` for subtests** - This is 100% consistent in this codebase
2. **Use descriptive test names** - The `name` field should clearly describe what is being tested
3. **Test one thing per case** - Each table entry should test one specific behavior
4. **Include edge cases** - Empty strings, nil values, maximum values, etc.
5. **Use `errors.Is` for error comparison** - Not `==` or `reflect.DeepEqual`
6. **Prefer `t.Errorf` over `t.Fatalf`** - See all failures before stopping
7. **Keep test data inline** - External files only for large golden test sets
## Common Pitfalls to Avoid
1. **Forgetting `t.Run()`** - Without subtests, all failures appear at the same line
2. **Using `Fatalf` immediately** - You won't see other test failures
3. **Not capturing range variable** - In Go < 1.22, add `tt := tt` before `t.Run`
4. **Anonymous structs** - This codebase prefers named structs for clarity
5. **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
```bash
# 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
- [Go Wiki: TableDrivenTests](https://go.dev/wiki/TableDrivenTests)
- [Go Testing Package](https://pkg.go.dev/testing/)
- [Prefer Table Driven Tests - Dave Cheney](https://dave.cheney.net/2019/05/07/prefer-table-driven-tests)
Name Size