| name | go-table-driven-tests |
| description | Write Go table-driven tests following established patterns. Use when writing tests, creating test functions, adding test cases, or when the user mentions "test", "table-driven", "Go tests", or testing in Go codebases. |
Go Table-Driven Tests
Use this skill when writing or modifying Go table-driven tests. It ensures tests follow established patterns.
Core Principles
- One test function, many cases - Define test cases in a slice and iterate with
t.Run()
- Explicit naming - Each case has a
name field that becomes the subtest name
- Structured inputs - Use struct fields for inputs, expected outputs, and configuration
- Helper functions - Use
t.Helper() in test helpers for proper line reporting
- Environment guards - Skip integration tests when credentials are unavailable
Table Structure Pattern
func TestFunctionName(t *testing.T) {
tests := []struct {
name string
input Type
want Type
wantErr error
errCheck func(error) bool
setupEnv func() func()
}{
{
name: "descriptive case name",
input: "test input",
want: "expected output",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
})
}
}
Field Guidelines
| Field | Required | Purpose |
|---|
name | Yes | Subtest name - be descriptive and specific |
input/args | Varies | Input values for the function under test |
want/want* | Varies | Expected output values (e.g., wantErr, wantResult) |
errCheck | No | Custom error validation function |
setupEnv | No | Environment setup function returning cleanup |
Naming Conventions
- Test function:
Test<FunctionName> or Test<FunctionName>_<Scenario>
- Subtest names: lowercase, descriptive, spaces allowed
- Input fields: match parameter names or use
input/args
- Output fields: prefix with
want (e.g., want, wantErr, wantResult)
Common Patterns
1. Basic Table Test
func TestWithRegion(t *testing.T) {
tests := []struct {
name string
region string
}{
{"auto region", "auto"},
{"us-west-2", "us-west-2"},
{"eu-central-1", "eu-central-1"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
o := &Options{}
WithRegion(tt.region)(o)
if o.Region != tt.region {
t.Errorf("Region = %v, want %v", o.Region, tt.region)
}
})
}
}
2. Error Checking with wantErr
func TestNew_errorCases(t *testing.T) {
tests := []struct {
name string
input string
wantErr error
}{
{"empty input", "", ErrInvalidInput},
{"invalid input", "!!!", ErrInvalidInput},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := Parse(tt.input)
if !errors.Is(err, tt.wantErr) {
t.Errorf("error = %v, want %v", err, tt.wantErr)
}
})
}
}
3. Custom Error Validation with errCheck
func TestNew_customErrors(t *testing.T) {
tests := []struct {
name string
setupEnv func() func()
wantErr error
errCheck func(error) bool
}{
{
name: "no bucket name returns ErrNoBucketName",
setupEnv: func() func() { return func() {} },
wantErr: ErrNoBucketName,
errCheck: func(err error) bool {
return errors.Is(err, ErrNoBucketName)
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cleanup := tt.setupEnv()
defer cleanup()
_, err := New(context.Background())
if tt.wantErr != nil {
if tt.errCheck != nil {
if !tt.errCheck(err) {
t.Errorf("error = %v, want %v", err, tt.wantErr)
}
}
}
})
}
}
4. Environment Setup with setupEnv
func TestNew_envVarOverrides(t *testing.T) {
tests := []struct {
name string
setupEnv func() func()
options []Option
wantErr error
}{
{
name: "bucket from env var",
setupEnv: func() func() {
os.Setenv("TIGRIS_STORAGE_BUCKET", "test-bucket")
return func() { os.Unsetenv("TIGRIS_STORAGE_BUCKET") }
},
wantErr: nil,
},
{
name: "bucket from option overrides env var",
setupEnv: func() func() {
os.Setenv("TIGRIS_STORAGE_BUCKET", "env-bucket")
return func() { os.Unsetenv("TIGRIS_STORAGE_BUCKET") }
},
options: []Option{
func(o *Options) { o.BucketName = "option-bucket" },
},
wantErr: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cleanup := tt.setupEnv()
defer cleanup()
_, err := New(context.Background(), tt.options...)
if tt.wantErr != nil && !errors.Is(err, tt.wantErr) {
t.Errorf("error = %v, want %v", err, tt.wantErr)
}
})
}
}
Integration Test Guards
For tests requiring real credentials, use a skip helper:
func skipIfNoCreds(t *testing.T) {
t.Helper()
if os.Getenv("TIGRIS_STORAGE_ACCESS_KEY_ID") == "" ||
os.Getenv("TIGRIS_STORAGE_SECRET_ACCESS_KEY") == "" {
t.Skip("skipping: TIGRIS_STORAGE_ACCESS_KEY_ID and TIGRIS_STORAGE_SECRET_ACCESS_KEY not set")
}
}
func TestCreateBucket(t *testing.T) {
tests := []struct {
name string
bucket string
options []BucketOption
wantErr error
}{
{
name: "create snapshot-enabled bucket",
bucket: "test-bucket",
options: []BucketOption{WithEnableSnapshot()},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
skipIfNoCreds(t)
})
}
}
Test Helpers
Use t.Helper() in helper functions for proper line number reporting:
func setupTestBucket(t *testing.T, ctx context.Context, client *Client) string {
t.Helper()
skipIfNoCreds(t)
bucket := "test-bucket-" + randomSuffix()
err := client.CreateBucket(ctx, bucket)
if err != nil {
t.Fatalf("failed to create test bucket: %v", err)
}
return bucket
}
func cleanupTestBucket(t *testing.T, ctx context.Context, client *Client, bucket string) {
t.Helper()
err := client.DeleteBucket(ctx, bucket, WithForceDelete())
if err != nil {
t.Logf("warning: failed to cleanup test bucket %s: %v", bucket, err)
}
}
Checklist
When writing table-driven tests:
Best Practices
Detailed Error Messages
Include both actual and expected values in error messages for clear failure diagnosis:
t.Errorf("got %q, want %q", actual, expected)
Note: t.Errorf is not an assertion - the test continues after logging. This helps identify whether failures are systematic or isolated to specific cases.
Maps for Test Cases
Consider using a map instead of a slice for test cases. Map iteration order is non-deterministic, which ensures test cases are truly independent:
tests := map[string]struct {
input string
want string
}{
"empty string": {input: "", want: ""},
"single character": {input: "x", want: "x"},
"multi-byte glyph": {input: "🎉", want: "🎉"},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
got := process(tt.input)
if got != tt.want {
t.Errorf("got %q, want %q", got, tt.want)
}
})
}
Parallel Testing
Add t.Parallel() calls to run test cases in parallel. The loop variable is automatically captured per iteration:
func TestFunction(t *testing.T) {
tests := []struct {
name string
input string
}{
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
})
}
}
References