Go 语言中,单元测试是通过标准库中的 testing 包来实现的,该包提供了一组功能,使得编写、运行和管理单元测试变得简单和高效。
一、规则
-
测试文件的命名规则
Go 中的测试文件命名规则是在被测试的源文件名后面加上
_test.go
。例如,如果你有一个calculator.go
文件,相应的测试文件应该是calculator_test.go
。 -
测试函数的命名规则
测试函数必须以
Test
开头,后面可以跟任何非空字符串,例如TestAdd
、TestSubtract
等。 -
使用
testing.T
进行断言和错误报告在测试函数中,使用
testing.T
类型的参数来管理测试状态和输出。你可以使用t.Error*
、t.Fail*
等方法来指示测试失败,并输出相关的错误信息。
二、单元测试示例
2.1 单个测试用例
在calculator包中定义了一个calculator函数,具体实现如下:
go
package calculator
func Add(a, b int) int {
return a + b
}
在当前目录下,我们创建一个calculator_test.go的测试文件,并定义一个测试函数如下:
go
package calculator
import "testing"
func TestAdd(t *testing.T) {
result := Add(1, 2)
expected := 3
if result != expected {
t.Errorf("Add(1,2) return %d, expected %d", result, expected)
}
}
要运行这个单元测试,可以使用 go test 命令。在命令行中进入到包含 calculator.go 和 calculator_test.go 的目录,然后执行go test
,这些结果如下
go
go test
PASS
ok modu 0.226s
2.2 多个测试用例
在calculator_test.go中添加如下测试函数:
go
func TestAdd2(t *testing.T) {
result := Add(3, 2)
expected := 3
if result != expected {
t.Errorf("Add(1,2) return %d, expected %d", result, expected)
}
}
为了能更好的在输出结果中看到每个测试用例的执行情况,我们可以为go test -v
参数,让它输出完整的测试结果。
go
go test -v
=== RUN TestAdd
--- PASS: TestAdd (0.00s)
=== RUN TestAdd2
calculator_test.go:17: Add(1,2) return 5, expected 3
--- FAIL: TestAdd2 (0.00s)
FAIL
exit status 1
FAIL modu 0.216s
2.3 指定运行测试用例
go test -run
命令可以按照指定的模式运行测试。这个命令支持通过正则表达式来选择要运行的测试函数。
例如修正好TestAdd2用例之后,通过go tes -run=Add2
只运行TestAdd2这个测试用例,结果是
go
go test -run=Add2 -v
=== RUN TestAdd2
--- PASS: TestAdd2 (0.00s)
PASS
ok modu 0.198s
4. 跳过某些测试用例
新加测试函数
go
func TestAdd3(t *testing.T) {
if testing.Short() {
t.Skip("short模式下会跳过该测试用例")
}
result := Add(3, 2)
expected := 5
if result != expected {
t.Errorf("Add(1,2) return %d, expected %d", result, expected)
}
}
当执行go test -shor
t时,就会跳过testing.Short()
标记的测试用例,结果是
go
go test -short -v
=== RUN TestAdd
--- PASS: TestAdd (0.00s)
=== RUN TestAdd2
--- PASS: TestAdd2 (0.00s)
=== RUN TestAdd3
calculator_test.go:23: short模式下会跳过该测试用例
--- SKIP: TestAdd3 (0.00s)
PASS
ok modu 0.635s
三、测试组和子测试
3.1 测试组和子测试
通过测试组和子测试,可以更友好来添加更多的测试用例,以及查看结果
go
func TestAdd(t *testing.T) {
tests := []struct {
name string
x, y int
expected int
}{
{"Add1", 1, 2, 3},
{"Add2", 3, 3, 6},
{"Add3", 4, 5, 8},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result := Add(tc.x, tc.y)
if result != tc.expected {
t.Errorf("Add(%d, %d) returned %d, expected %d", tc.x, tc.y, result, tc.expected)
}
})
}
}
运行go test -v
,结果是
go
go test -v
=== RUN TestAdd
=== RUN TestAdd/Add2
=== RUN TestAdd/Add3
calculator_test.go:51: Add(4, 5) returned 9, expected 8
--- FAIL: TestAdd (0.00s)
--- PASS: TestAdd/Add1 (0.00s)
--- PASS: TestAdd/Add2 (0.00s)
--- FAIL: TestAdd/Add3 (0.00s)
FAIL
exit status 1
FAIL modu 0.190s
3.2 并行测试
Go语言天生支持并发,所以通过添加t.Parallel()
来实现驱动测试并行化。
go
func TestAdd(t *testing.T) {
t.Parallel() // 将 TLog 标记为能够与其他测试并行运行
// 这里使用匿名结构体定义了若干个测试用例
// 并且为每个测试用例设置了一个名称
tests := []struct {
name string
x, y int
expected int
}{
{"Add1", 1, 2, 3},
{"Add2", 3, 3, 6},
{"Add3", 4, 5, 8},
}
for _, tc := range tests {
tc := tc // 注意这里重新声明tt变量(避免多个goroutine中使用了相同的变量)
t.Run(tc.name, func(t *testing.T) {
t.Parallel() // 将每个测试用例标记为能够彼此并行运行
result := Add(tc.x, tc.y)
if result != tc.expected {
t.Errorf("Add(%d, %d) returned %d, expected %d", tc.x, tc.y, result, tc.expected)
}
})
}
}
3.3 测试覆盖率
使用go test -cover
来查看测试覆盖率
go
go test -cover
PASS
modu coverage: 100.0% of statements
ok modu 1.149s
四、Go单元测试工具包 -- testify
在进行Go语言单元测试时,由于官方并未内置断言功能,我们通常需要使用大量的if...else...
语句来校验测试结果。然而,通过使用第三方库如testify/assert
,我们可以轻松地调用多种常用的断言函数,这些函数不仅能够简化测试代码,还能生成清晰易懂的错误描述信息,帮助我们快速定位问题。
在上面例子当中,我们使用if...else...
语句来校验测试结果
go
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result := Add(tc.x, tc.y)
if result != tc.expected {
t.Errorf("Add(%d, %d) returned %d, expected %d", tc.x, tc.y, result, tc.expected)
}
})
}
现在可使用testify/assert
之将上述判断过程简化如下:
go
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result := Add(tc.x, tc.y)
assert.Equal(t, result, tc.expected)
})
}
testify/require
拥有testify/assert
所有断言函数,它们的唯一区别就是testify/require
遇到失败的用例会立即终止本次测试。