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函数,具体实现如下:
package calculatorfunc Add(a, b int) int {return a + b
}
在当前目录下,我们创建一个calculator_test.go的测试文件,并定义一个测试函数如下:
package calculatorimport "testing"func TestAdd(t *testing.T) {result := Add(1, 2)expected := 3if result != expected {t.Errorf("Add(1,2) return %d, expected %d", result, expected)}
}
要运行这个单元测试,可以使用 go test 命令。在命令行中进入到包含 calculator.go 和 calculator_test.go 的目录,然后执行go test
,这些结果如下
go test
PASS
ok modu 0.226s
2.2 多个测试用例
在calculator_test.go中添加如下测试函数:
func TestAdd2(t *testing.T) {result := Add(3, 2)expected := 3if result != expected {t.Errorf("Add(1,2) return %d, expected %d", result, expected)}
}
为了能更好的在输出结果中看到每个测试用例的执行情况,我们可以为go test -v
参数,让它输出完整的测试结果。
go test -v
=== RUN TestAdd
--- PASS: TestAdd (0.00s)
=== RUN TestAdd2calculator_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 test -run=Add2 -v
=== RUN TestAdd2
--- PASS: TestAdd2 (0.00s)
PASS
ok modu 0.198s
4. 跳过某些测试用例
新加测试函数
func TestAdd3(t *testing.T) {if testing.Short() {t.Skip("short模式下会跳过该测试用例")}result := Add(3, 2)expected := 5if result != expected {t.Errorf("Add(1,2) return %d, expected %d", result, expected)}
}
当执行go test -shor
t时,就会跳过testing.Short()
标记的测试用例,结果是
go test -short -v
=== RUN TestAdd
--- PASS: TestAdd (0.00s)
=== RUN TestAdd2
--- PASS: TestAdd2 (0.00s)
=== RUN TestAdd3calculator_test.go:23: short模式下会跳过该测试用例
--- SKIP: TestAdd3 (0.00s)
PASS
ok modu 0.635s
三、测试组和子测试
3.1 测试组和子测试
通过测试组和子测试,可以更友好来添加更多的测试用例,以及查看结果
func TestAdd(t *testing.T) {tests := []struct {name stringx, y intexpected 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 test -v
=== RUN TestAdd
=== RUN TestAdd/Add2
=== RUN TestAdd/Add3calculator_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()
来实现驱动测试并行化。
func TestAdd(t *testing.T) {t.Parallel() // 将 TLog 标记为能够与其他测试并行运行// 这里使用匿名结构体定义了若干个测试用例// 并且为每个测试用例设置了一个名称tests := []struct {name stringx, y intexpected 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 test -cover
PASSmodu coverage: 100.0% of statements
ok modu 1.149s
四、Go单元测试工具包 – testify
在进行Go语言单元测试时,由于官方并未内置断言功能,我们通常需要使用大量的if...else...
语句来校验测试结果。然而,通过使用第三方库如testify/assert
,我们可以轻松地调用多种常用的断言函数,这些函数不仅能够简化测试代码,还能生成清晰易懂的错误描述信息,帮助我们快速定位问题。
在上面例子当中,我们使用if...else...
语句来校验测试结果
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
之将上述判断过程简化如下:
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
遇到失败的用例会立即终止本次测试。