4、表格驱动测试:让测试代码更优雅

说实话,我刚写Go测试那会儿,每个测试用例都是单独写一个函数。测个加法函数,写了七八个TestAdd_WithPositiveNumbersTestAdd_WithNegativeNumbers……代码又臭又长。后来看到Go标准库里的表格驱动测试,我才恍然大悟——原来测试可以这么清爽。

什么是表格驱动测试

表格驱动测试,说白了就是把测试用例组织成一张「表格」。每个用例包含输入和期望输出,然后统一遍历执行。这样做的好处很明显:

  • 减少重复代码:测试逻辑写一次就够了
  • 新增用例零成本:往表格里加一行就行
  • 可读性极强:一眼就能看到所有测试场景

我在项目中遇到过一位同事,他写测试喜欢复制粘贴。测一个函数,复制了20遍几乎一样的代码,只是参数不同。后来我给他看了表格驱动测试,他当场就重构了——代码从300行缩到了50行。

定义测试用例结构体

先看一个最简单的例子。假设我们要测试一个加法函数:

func Add(a, b int) int {
    return a + b
}

传统的测试写法是这样的:

func TestAdd(t *testing.T) {
    if Add(1, 2) != 3 {
        t.Errorf("Add(1, 2) = %d, want %d", Add(1, 2), 3)
    }
    if Add(-1, 1) != 0 {
        t.Errorf("Add(-1, 1) = %d, want %d", Add(-1, 1), 0)
    }
    if Add(0, 0) != 0 {
        t.Errorf("Add(0, 0) = %d, want %d", Add(0, 0), 0)
    }
}

嗯,这里要注意——如果我有20个用例,这代码得膨胀成什么样?

用表格驱动测试改写一下:

func TestAdd(t *testing.T) {
    // 定义测试用例结构体
    tests := []struct {
        name string
        a    int
        b    int
        want int
    }{
        {name: "正数相加", a: 1, b: 2, want: 3},
        {name: "正负数相加", a: -1, b: 1, want: 0},
        {name: "零值相加", a: 0, b: 0, want: 0},
        {name: "大数相加", a: 1000000, b: 2000000, want: 3000000},
    }

    // 遍历测试用例执行
    for _, tt := range tests {
        got := Add(tt.a, tt.b)
        if got != tt.want {
            t.Errorf("%s: Add(%d, %d) = %d, want %d",
                tt.name, tt.a, tt.b, got, tt.want)
        }
    }
}

核心要点:测试用例结构体通常包含name(用例名称)、输入参数和期望输出。结构体字段可以根据被测函数灵活定义。

遍历测试用例执行

遍历执行这部分,我个人习惯用for range循环。每个用例执行完,如果失败就打印错误信息。但这里有个坑——一旦某个用例失败,后面的用例还会继续执行。这其实是好事,因为你能看到所有失败的用例,而不是卡在第一个就停了。

我曾经踩过一个坑:测试数据库操作时,用例之间有依赖关系。第一个用例插入数据,第二个用例查询数据。结果第一个用例失败了,第二个用例也跟着报错。排查了半天才发现是测试用例的顺序问题。后来我学乖了——表格驱动测试的每个用例应该是独立的,不要互相依赖。

结合子测试(t.Run)进行精细化测试

上面的写法有个小问题:如果某个用例失败了,你只知道它失败了,但不知道具体是哪个。虽然我们在错误信息里打印了name,但Go的测试框架并不会把它当作独立的测试项。

这时候就该t.Run出场了。它能把每个用例变成独立的子测试:

func TestAdd(t *testing.T) {
    tests := []struct {
        name string
        a    int
        b    int
        want int
    }{
        {name: "正数相加", a: 1, b: 2, want: 3},
        {name: "正负数相加", a: -1, b: 1, want: 0},
        {name: "零值相加", a: 0, b: 0, want: 0},
    }

    for _, tt := range tests {
        // 使用 t.Run 创建子测试
        t.Run(tt.name, func(t *testing.T) {
            got := Add(tt.a, tt.b)
            if got != tt.want {
                t.Errorf("Add(%d, %d) = %d, want %d",
                    tt.a, tt.b, got, tt.want)
            }
        })
    }
}

运行结果会变成这样:

=== RUN   TestAdd
=== RUN   TestAdd/正数相加
=== RUN   TestAdd/正负数相加
=== RUN   TestAdd/零值相加
--- PASS: TestAdd (0.00s)
    --- PASS: TestAdd/正数相加 (0.00s)
    --- PASS: TestAdd/正负数相加 (0.00s)
    --- PASS: TestAdd/零值相加 (0.00s)

你想想看,如果某个用例失败了,你能直接看到是哪个子测试出了问题。而且还能单独运行某个子测试:

go test -run TestAdd/正数相加

小技巧:子测试的名称最好用有意义的字符串,不要用数字索引。这样在日志里一眼就能看出是哪个场景出了问题。

更复杂的场景:多返回值测试

实际项目中,函数往往有多个返回值。比如一个除法函数,既要返回结果,也要返回错误:

func Divide(a, b int) (int, error) {
    if b == 0 {
        return 0, errors.New("除数不能为0")
    }
    return a / b, nil
}

测试用例结构体可以这样设计:

func TestDivide(t *testing.T) {
    tests := []struct {
        name    string
        a       int
        b       int
        want    int
        wantErr bool
    }{
        {name: "正常除法", a: 10, b: 2, want: 5, wantErr: false},
        {name: "除数为0", a: 10, b: 0, want: 0, wantErr: true},
        {name: "结果为0", a: 0, b: 5, want: 0, wantErr: false},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got, err := Divide(tt.a, tt.b)
            if tt.wantErr {
                if err == nil {
                    t.Errorf("期望错误,但没有得到错误")
                }
                return
            }
            if err != nil {
                t.Errorf("不期望错误,但得到: %v", err)
            }
            if got != tt.want {
                t.Errorf("Divide(%d, %d) = %d, want %d",
                    tt.a, tt.b, got, tt.want)
            }
        })
    }
}

注意:测试错误场景时,记得用return提前退出。否则如果got是零值,后面的断言可能会误报。

表格驱动测试的最佳实践

总结一下我这些年用表格驱动测试的心得:

  1. 用例要独立:每个用例不依赖其他用例的执行结果
  2. 命名要有意义:用中文或英文描述清楚测试场景
  3. 覆盖边界值:空值、零值、最大值、最小值都要测到
  4. 错误场景也要测:别只测正常流程,异常流程更重要
  5. 善用子测试t.Run让测试报告更清晰

我曾经接手过一个遗留项目,测试覆盖率号称80%,但全是重复的、硬编码的测试用例。我花了一周时间全部改成表格驱动测试,代码量减少了60%,覆盖率反而提升到了95%。说白了,表格驱动测试不只是让代码变短,更重要的是让测试意图变得清晰。

下一章我们会聊测试覆盖率——怎么衡量你的测试够不够好,以及常见的覆盖率陷阱。到时候见。