表驱动测试因其简洁、清晰和高效而成为 Golang 中的流行技术。 它消除了冗余并能够以最少的代码重复来测试各种情况。
为什么使用表驱动测试?
减少样板代码
与其他测试方法(例如,参数化测试或数据驱动测试)相比,表驱动测试所需的代码更少。它无需重复的测试用例定义,使开发者能够专注于函数的核心逻辑。
可读性提高
通过使用表定义测试用例,可以容易地看到正在测试的输入内容以及预期的输出内容应该是什么。这使得更容易了解函数的行为并发现潜在的问题。
更好地覆盖率
能够使用更少的测试用例来覆盖广泛的输入和输出。在处理复杂或边缘案例时,这一点尤其有用,因为它们可以很容易地放入表中。
更轻松的维护
如果函数行为发生变化,只需更新输入表格即可。这使得更容易维护测试并确保它们能继续涵盖所有相关场景。
改进测试数据管理
以结构化格式定义测试用例,帮助管理大量测试数据。在使用包含多行或多列输入数据的数据集时,这尤其有用。
最佳实践
构建表格
定义一个专用于表示测试用例的类型。包含输入、预期输出和可选说明性名称的字段。
这是一个带有两个数字相加的函数的简单测试表的示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
package tdd
import (
"github.com/stretchr/testify/assert"
"math"
"testing"
)
func add(a, b int) int {
return a + b
}
type addTest struct {
name string
a int
b int
expected int
}
func TestAdd(t *testing.T) {
tests := []addTest{
{"1+2", 1, 2, 3},
{"-1+-2", -1, -2, -3},
{"0+0", 0, 0, 0},
{"edge case: infinity", int(math.Inf(1)), int(math.Inf(-1)), 0},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result := add(test.a, test.b)
assert.Equal(t, test.expected, result)
})
}
}
|
在这个例子中,我们定义了一个 addTest
结构,其中包含 add
函数的输入和预期输出。然后,我们定义一个名为 tests
的 addTest
对象切片,我们在测试循环中使用它进行迭代。对于每个测试用例,我们使用输入值调用 add
函数并将结果与预期输出进行比较。
测试边缘情况
表测试的另一个重要方面是确保测试边缘情况和极端情况。边缘情况的示例包括:
- 负数
- 零值
- NaN(非数字)值
- 大数
- 小数字
- 正无穷
- 负无穷
这是一个如何修改我们的上一个测试以包含边界情况的示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
func TestAdd(t *testing.T) {
tests := []addTest{
{"1+2", 1, 2, 3},
{"-1+-2", -1, -2, -3},
{"edge case: zero values", 0, 0, 0},
{"edge case: infinity", int(math.Inf(1)), int(math.Inf(-1)), 0},
{"large int input", math.MaxInt64, 1, -9223372036854775808},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result := add(test.a, test.b)
assert.Equal(t, test.expected, result)
})
}
}
|
在这个例子中,我们添加了一个新的测试用例来测试当输入值非常大时会发生什么。我们使用 math.MaxInt64
常量来表示一个非常大的整数值,并在其上加 1 以创建一个更大的输入。这个测试用例确保我们的函数在使用非常大的数字时能正确运行。
使用基准来衡量性能
基准是 Go 中表驱动单元测试的一个重要方面。通过使用基准,可以衡量函数在处理大量数据时的性能。根据基准测试结果可以优化相关的函数以获得更好的性能,并确保它在不同条件下都能正确运行。
这是一个如何修改我们的上一个测试以包含基准的示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
func BenchmarkAdd(b *testing.B) {
tests := []addTest{
{"1+2", 1, 2, 3},
{"-1+-2", -1, -2, -3},
{"0+0", 0, 0, 0},
{"math.Inf(1) + math.Inf(-1)", int(math.Inf(1)), int(math.Inf(-1)), 0},
{"large int input", math.MaxInt64, 1, -9223372036854775808},
}
for i := 0; i < b.N; i++ {
for _, test := range tests {
b.Run(test.name, func(b *testing.B) {
result := add(test.a, test.b)
assert.Equal(b, test.expected, result)
})
}
}
}
|
在这个例子中,我们修改了我们以前的测试以包括使用 testing.B
结构的基准。我们定义了一个新的 BenchmarkAdd
函数,它接受一个 testing.B
对象并使用它来迭代我们的 addTest
对象切片。对于每个测试用例,我们使用输入值调用 add
函数并将结果与预期输出进行比较。
保持简洁
把表的大小限制在一系列合理数量的用例内。若有多余的部分,考虑将它们拆分成单独的功能或子表。
给测试命名
为每个测试用例命名 name
。这可以提高可读性和理解。
1
2
3
4
5
6
|
type addTest struct {
name string
a int
b int
expected int
}
|
关注核心逻辑
避免在表测试函数内进行复杂的设置或拆卸逻辑。将必要的准备和断言提取到辅助函数中。
考虑子测试
如果有复杂的情况,考虑使用子测试,以获得更好的错误报告和对故障的细节理解。
使用泛型定义的 TestCase
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
|
package tdd
import (
"errors"
"github.com/stretchr/testify/assert"
"testing"
)
var (
errDivisionByZero = errors.New("division by zero")
)
type TestCase[P any, W any] struct {
Name string
Params P
Want W
Err error
}
type params struct {
a int
b int
}
func divide(a, b int) (int, error) {
if b == 0 {
return 0, errDivisionByZero
}
return a / b, nil
}
func TestDivide(t *testing.T) {
tests := []TestCase[params, int]{
{
Name: "positive division",
Params: params{10, 2},
Want: 5,
},
{
Name: "negative division",
Params: params{-10, 2},
Want: -5,
},
{
Name: "division by one",
Params: params{10, 1},
Want: 10,
},
{
Name: "division by zero",
Params: params{10, 0},
Want: 0,
Err: errDivisionByZero,
},
}
for _, test := range tests {
t.Run(test.Name, func(t *testing.T) {
want, err := divide(test.Params.a, test.Params.b)
if err != nil {
assert.Equal(t, test.Err, err)
}
assert.Equal(t, test.Want, want)
})
}
}
|