关于 go-table-driven-tests
面向编写和修改 Go 表驱动测试的 Skill,帮助开发者统一测试用例、子测试与辅助函数的写法。内容也涵盖环境清理和无凭据时跳过集成测试。
主要功能
- 01用测试表组织用例
- 02通过 t.Run() 运行命名用例
- 03检查预期结果和错误
- 04设置并清理测试环境
- 05无凭据时跳过集成测试
适用场景
- 01为 Go 函数编写表驱动测试
- 02检查成功和错误场景
- 03测试环境变量行为
- 04跳过需要凭据的测试
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ tigrisdata/ skills --skill go-table-driven-tests安装后在 Claude Code 中输入/go-table-driven-tests确认已加载。
附带文件metadata.jsonREADME.md
详细介绍
Go 表驱动测试
在编写或修改 Go 表驱动测试时使用此技能,确保测试遵循既定模式。
核心原则
- 一个测试函数,多个用例 —— 将测试用例定义在切片中,并通过
t.Run()遍历 - 明确命名 —— 每个用例都有一个
name字段,作为子测试名称 - 结构化输入 —— 使用结构体字段表示输入、预期输出和配置
- 辅助函数 —— 在测试辅助函数中使用
t.Helper(),以便正确报告行号 - 环境保护 —— 缺少凭据时跳过集成测试
表结构模式
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
})
}
}
字段指南
| 字段 | 必需 | 用途 |
|---|---|---|
name | 是 | 子测试名称——应具体且明确 |
input/args | 视情况而定 | 被测函数的输入值 |
want/want* | 视情况而定 | 预期输出值(例如,wantErr、wantResult) |
errCheck | 否 | 自定义错误验证函数 |
setupEnv | 否 | 返回清理函数的环境设置函数 |
命名约定
- 测试函数:
Test<FunctionName>或Test<FunctionName>_<Scenario> - 子测试名称:小写、描述清晰,可包含空格
- 输入字段:与参数名称一致,或使用
input/args - 输出字段:使用
want作为前缀(例如,want、wantErr、wantResult)
常见模式
1. 基本表驱动测试
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. 使用 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. 使用 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. 使用 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)
}
})
}
}
集成测试保护
对于需要真实凭据的测试,使用跳过辅助函数:
// 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
})
}
}
测试辅助函数
在辅助函数中使用 t.Helper(),以便正确报告行号:
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)
}
}
检查清单
编写表驱动测试时:
- 表结构体将
name字段作为第一个字段 - 每个测试用例都有明确的名称
- 输入字段使用清晰的命名(与参数一致或使用
input) - 预期输出字段以
want为前缀 - 使用
t.Run(tt.name, func(t *testing.T) { ... })遍历 - 使用
errors.Is()比较错误 - 环境设置通过
defer执行清理 - 集成测试使用
skipIfNoCreds(t)辅助函数 - 测试辅助函数使用
t.Helper(),以便正确报告行号 - 测试文件名为
*_test.go,并与被测代码放在一起
最佳实践
详细的错误消息
在错误消息中同时包含实际值和预期值,以便清晰地诊断失败原因:
t.Errorf("got %q, want %q", actual, expected)
注意: t.Errorf 不是断言——记录错误后,测试仍会继续。这有助于判断失败是系统性的,还是仅发生在特定用例中。
使用 Map 存放测试用例
可以考虑使用 map 而不是切片存放测试用例。Map 的迭代顺序不确定,可确保测试用例彼此独立:
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)
}
})
}
并行测试
添加 t.Parallel() 调用,以并行运行测试用例。每次迭代都会自动捕获对应的循环变量:
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
})
}
}
参考资料
- Go Wiki:表驱动测试 - Go 社区关于表驱动测试的官方最佳实践
- Go Testing 包 - 标准库测试文档
- 优先使用表驱动测试 - Dave Cheney 撰写的指南,介绍何时以及为何应优先使用表驱动测试,而非传统测试结构
评论
支持 Markdown