go-table-driven-tests

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Go 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
    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
  • 一个测试函数,多组测试用例 - 在切片中定义测试用例,并使用
    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

字段指南

FieldRequiredPurpose
name
YesSubtest name - be descriptive and specific
input
/
args
VariesInput values for the function under test
want
/
want*
VariesExpected output values (e.g.,
wantErr
,
wantResult
)
errCheck
NoCustom error validation function
setupEnv
NoEnvironment setup function returning cleanup
字段名是否必填用途
name
子测试名称 - 描述性强且具体
input
/
args
可选被测函数的输入值
want
/
want*
可选预期输出值(例如
wantErr
wantResult
errCheck
自定义错误校验函数
setupEnv
环境初始化函数,返回清理函数

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
    )
  • 测试函数:
    Test<FunctionName>
    Test<FunctionName>_<Scenario>
  • 子测试名称:小写、描述性强,允许使用空格
  • 输入字段:与参数名一致,或使用
    input
    /
    args
  • 输出字段:以
    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

2. 使用
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)
            }
        })
    }
}
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

3. 使用
errCheck
进行自定义错误校验

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)
                    }
                }
            }
        })
    }
}
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

4. 使用
setupEnv
进行环境初始化

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)
            }
        })
    }
}
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
t.Helper()
in helper functions for proper line number reporting:
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
    name
    field as first field
  • 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
    errors.Is()
    for error comparison
  • Environment setup includes cleanup in
    defer
  • Integration tests use
    skipIfNoCreds(t)
    helper
  • Test helpers use
    t.Helper()
    for proper line reporting
  • Test file is
    *_test.go
    and lives next to the code it tests
编写表格驱动测试时:
  • 表格结构体的第一个字段为
    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:
t.Errorf
is not an assertion - the test continues after logging. This helps identify whether failures are systematic or isolated to specific cases.
在错误信息中包含实际值和预期值,便于定位故障原因:
go
t.Errorf("got %q, want %q", actual, expected)
注意:
t.Errorf
不是断言 - 记录错误后测试会继续执行。这有助于判断故障是系统性问题还是仅存在于特定用例中。

Maps 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
t.Parallel()
calls to run test cases in parallel. The loop variable is automatically captured per iteration:
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

参考资料