mcpField
G
go-table-driven-tests

go-table-driven-tests

github.com/tigrisdata测试与质量293 次安装14 周前更新
代码仓库

关于 go-table-driven-tests

面向编写和修改 Go 表驱动测试的 Skill,帮助开发者统一测试用例、子测试与辅助函数的写法。内容也涵盖环境清理和无凭据时跳过集成测试。

主要功能

  1. 01用测试表组织用例
  2. 02通过 t.Run() 运行命名用例
  3. 03检查预期结果和错误
  4. 04设置并清理测试环境
  5. 05无凭据时跳过集成测试

适用场景

  1. 01为 Go 函数编写表驱动测试
  2. 02检查成功和错误场景
  3. 03测试环境变量行为
  4. 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
        })
    }
}

参考资料

评论

支持 Markdown