go-table-driven-tests
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseGo Table-Driven Tests
Go表格驱动测试
Use this skill when writing or modifying Go table-driven tests. It ensures tests follow established patterns.
编写或修改Go表格驱动测试时可使用此方法,确保测试遵循既定模式。
Core Principles
核心原则
- One test function, many cases - Define test cases in a slice and iterate with
t.Run() - Explicit naming - Each case has a field that becomes the subtest name
name - Structured inputs - Use struct fields for inputs, expected outputs, and configuration
- Helper functions - Use in test helpers for proper line reporting
t.Helper() - Environment guards - Skip integration tests when credentials are unavailable
- 一个测试函数,多组测试用例 - 在切片中定义测试用例,并使用进行遍历
t.Run() - 明确命名 - 每个测试用例包含字段,作为子测试名称
name - 结构化输入 - 使用结构体字段存储输入、预期输出和配置信息
- 辅助函数 - 在测试辅助函数中使用以正确定位报错行号
t.Helper() - 环境校验 - 当凭证不可用时,跳过集成测试
Table Structure Pattern
表格结构模式
go
func TestFunctionName(t *testing.T) {
tests := []struct {
name string // required: subtest name
input Type // function input
want Type // expected output
wantErr error // expected error (nil for success)
errCheck func(error) bool // optional: custom error validation
setupEnv func() func() // optional: env setup, returns cleanup
}{
{
name: "descriptive case name",
input: "test input",
want: "expected output",
},
// ... more cases
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// test implementation using tt fields
})
}
}go
func TestFunctionName(t *testing.T) {
tests := []struct {
name string // required: subtest name
input Type // function input
want Type // expected output
wantErr error // expected error (nil for success)
errCheck func(error) bool // optional: custom error validation
setupEnv func() func() // optional: env setup, returns cleanup
}{
{
name: "descriptive case name",
input: "test input",
want: "expected output",
},
// ... more cases
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// test implementation using tt fields
})
}
}Field Guidelines
字段指南
| Field | Required | Purpose |
|---|---|---|
| Yes | Subtest name - be descriptive and specific |
| Varies | Input values for the function under test |
| Varies | Expected output values (e.g., |
| No | Custom error validation function |
| No | Environment setup function returning cleanup |
| 字段名 | 是否必填 | 用途 |
|---|---|---|
| 是 | 子测试名称 - 描述性强且具体 |
| 可选 | 被测函数的输入值 |
| 可选 | 预期输出值(例如 |
| 否 | 自定义错误校验函数 |
| 否 | 环境初始化函数,返回清理函数 |
Naming Conventions
命名规范
- Test function: or
Test<FunctionName>Test<FunctionName>_<Scenario> - Subtest names: lowercase, descriptive, spaces allowed
- Input fields: match parameter names or use /
inputargs - Output fields: prefix with (e.g.,
want,want,wantErr)wantResult
- 测试函数:或
Test<FunctionName>Test<FunctionName>_<Scenario> - 子测试名称:小写、描述性强,允许使用空格
- 输入字段:与参数名一致,或使用/
inputargs - 输出字段:以为前缀(例如
want、want、wantErr)wantResult
Common Patterns
常见模式
1. Basic Table Test
1. 基础表格测试
go
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)
}
})
}
}go
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
wantErr2. 使用wantErr
进行错误校验
wantErrgo
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)
}
})
}
}go
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
errCheck3. 使用errCheck
进行自定义错误校验
errCheckgo
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)
}
}
}
})
}
}go
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
setupEnv4. 使用setupEnv
进行环境初始化
setupEnvgo
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)
}
})
}
}go
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:
go
// skipIfNoCreds skips the test if Tigris credentials are not set.
// Use this for integration tests that require real Tigris operations.
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 implementation
})
}
}对于需要真实凭证的测试,使用跳过辅助函数:
go
// skipIfNoCreds skips the test if Tigris credentials are not set.
// Use this for integration tests that require real Tigris operations.
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 implementation
})
}
}Test Helpers
测试辅助函数
Use in helper functions for proper line number reporting:
t.Helper()go
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)
}
}在辅助函数中使用以正确显示报错行号:
t.Helper()go
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:
- Table struct has field as first field
name - Each test case has a descriptive name
- Input fields use clear naming (match parameters or use )
input - Expected output fields prefixed with
want - Iteration uses
t.Run(tt.name, func(t *testing.T) { ... }) - Error checking uses for error comparison
errors.Is() - Environment setup includes cleanup in
defer - Integration tests use helper
skipIfNoCreds(t) - Test helpers use for proper line reporting
t.Helper() - Test file is and lives next to the code it tests
*_test.go
编写表格驱动测试时:
- 表格结构体的第一个字段为
name - 每个测试用例都有描述性名称
- 输入字段命名清晰(与参数名一致或使用)
input - 预期输出字段以为前缀
want - 使用进行遍历
t.Run(tt.name, func(t *testing.T) { ... }) - 使用进行错误对比校验
errors.Is() - 环境初始化在中包含清理逻辑
defer - 集成测试使用辅助函数
skipIfNoCreds(t) - 测试辅助函数使用以正确显示行号
t.Helper() - 测试文件命名为,与被测代码放在同一目录
*_test.go
Best Practices
最佳实践
Detailed Error Messages
详细错误信息
Include both actual and expected values in error messages for clear failure diagnosis:
go
t.Errorf("got %q, want %q", actual, expected)Note: is not an assertion - the test continues after logging. This helps identify whether failures are systematic or isolated to specific cases.
t.Errorf在错误信息中包含实际值和预期值,便于定位故障原因:
go
t.Errorf("got %q, want %q", actual, expected)注意: 不是断言 - 记录错误后测试会继续执行。这有助于判断故障是系统性问题还是仅存在于特定用例中。
t.ErrorfMaps for Test Cases
使用Map存储测试用例
Consider using a map instead of a slice for test cases. Map iteration order is non-deterministic, which ensures test cases are truly independent:
go
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)
}
})
}可以考虑使用Map而非切片存储测试用例。Map的遍历顺序是不确定的,确保测试用例之间真正独立:
go
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 calls to run test cases in parallel. The loop variable is automatically captured per iteration:
t.Parallel()go
func TestFunction(t *testing.T) {
tests := []struct {
name string
input string
}{
// ... test cases
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel() // marks this subtest as parallel
// test implementation
})
}
}添加调用以并行运行测试用例,循环变量会自动按迭代捕获:
t.Parallel()go
func TestFunction(t *testing.T) {
tests := []struct {
name string
input string
}{
// ... test cases
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel() // 将此子标记为并行执行
// test implementation
})
}
}References
参考资料
- Go Wiki: TableDrivenTests - Official Go community best practices for table-driven testing
- Go Testing Package - Standard library testing documentation
- Prefer Table Driven Tests - Dave Cheney's guide on when and why to use table-driven tests over traditional test structures
- Go Wiki: TableDrivenTests - Go社区官方表格驱动测试最佳实践
- Go Testing Package - 标准库测试文档
- Prefer Table Driven Tests - Dave Cheney关于何时及为何使用表格驱动测试而非传统测试结构的指南