码丁实验室,一站式儿童编程学习产品,寻地方代理合作共赢,微信联系:leon121393608。
不写测试的开发不是好程序员。我个人非常崇尚TDD(Test Driven Development)的,然而可惜的是国内的程序员都不太关注测试这一部分。
这篇文章主要介绍下在Go语言中如何做单元测试。
go test
Go语言中的测试依赖go test
命令。编写测试代码和编写普通的Go代码过程是类似的,并不需要学习新的语法、规则或工具。
go test命令是一个按照一定约定和组织的测试代码的驱动程序。在包目录内,所有以_test.go
为后缀名的源代码文件都是go test
测试的一部分,不会被go build
编译到最终的可执行文件中。
在*_test.go
文件中有三种类型的函数,单元测试函数、基准测试函数和示例函数。
类型 | 格式 | 作用 |
---|---|---|
测试函数 | 函数名前缀为Test | 测试程序的一些逻辑行为是否正确 |
基准函数 | 函数名前缀为Benchmark | 测试函数的性能 |
示例函数 | 函数名前缀为Example | 为文档提供示例文档 |
go test
命令会遍历所有的*_test.go
文件中符合上述命名规则的函数,然后生成一个临时的main包用于调用相应的测试函数,然后构建并运行、报告测试结果,最后清理测试中生成的临时文件。
测试函数
每个测试函数必须导入testing
包,测试函数的基本格式(签名)如下:
func TestName(t *testing.T){
// ...
}
测试函数的名字必须以Test
开头,可选的后缀名必须以大写字母开头,举几个例子:
func TestAdd(t *testing.T){ ... }
func TestSum(t *testing.T){ ... }
func TestLog(t *testing.T){ ... }
其中参数t
用于报告测试失败和附加的日志信息。
接下来,我们定义一个split
的包,包中定义了一个Split
函数,具体实现如下:
split/split.go
package split
import "strings"
// split package with a single split function.
// Split slices s into all substrings separated by sep and
// returns a slice of the substrings between those separators.
func Split(s, sep string) (result []string) {
i := strings.Index(s, sep)
for i > -1 {
result = append(result, s[:i])
s = s[i+1:]
i = strings.Index(s, sep)
}
result = append(result, s)
return
}
在当前目录下,我们创建一个split_test.go
的测试文件,并定义一个测试函数如下:
split/split_test.go
package split
import (
"reflect"
"testing"
)
func TestSplit(t *testing.T) { // 测试函数名必须以Test开头,必须接收一个*testing.T类型参数
got := Split("a:b:c", ":") // 程序输出的结果
want := []string{"a", "b", "c"} // 期望的结果
if !reflect.DeepEqual(want, got) { // 因为slice不能比较直接,借助反射包中的方法比较
t.Errorf("excepted:%v, got:%v", want, got) // 测试失败输出错误提示
}
}
此时split
这个包中的文件如下:
split $ ls -l
total 16
-rw-r--r-- 1 liwenzhou staff 408 4 29 15:50 split.go
-rw-r--r-- 1 liwenzhou staff 466 4 29 16:04 split_test.go
在split
包路径下,执行go test
命令,可以看到输出结果如下:
split $ go test
PASS
ok github.com/Q1mi/studygo/code_demo/test_demo/split 0.005s
一个测试用例有点单薄,我们再编写一个测试使用多个字符切割字符串的例子,在split_test.go
中添加如下测试函数:
func TestMoreSplit(t *testing.T) {
got := Split("abcd", "bc")
want := []string{"a", "d"}
if !reflect.DeepEqual(want, got) {
t.Errorf("excepted:%v, got:%v", want, got)
}
}
再次运行go test
命令,输出结果如下:
split $ go test
--- FAIL: TestMultiSplit (0.00s)
split_test.go:20: excepted:[a d], got:[a cd]
FAIL
exit status 1
FAIL github.com/Q1mi/studygo/code_demo/test_demo/split 0.006s
这一次,我们的测试失败了。我们可以为go test
命令添加-v
参数,查看测试函数名称和运行时间:
split $ go test -v
=== RUN TestSplit
--- PASS: TestSplit (0.00s)
=== RUN TestMoreSplit
--- FAIL: TestMoreSplit (0.00s)
split_test.go:21: excepted:[a d], got:[a cd]
FAIL
exit status 1
FAIL github.com/Q1mi/studygo/code_demo/test_demo/split 0.005s
这一次我们能清楚的看到是TestMoreSplit
这个测试没有成功。
还可以在go test
命令后添加-run
参数,它对应一个正则表达式,只有函数名匹配上的测试函数才会被go test
命令执行。
split $ go test -v -run="More"
=== RUN TestMoreSplit
--- FAIL: TestMoreSplit (0.00s)
split_test.go:21: excepted:[a d], got:[a cd]
FAIL
exit status 1
FAIL github.com/Q1mi/studygo/code_demo/test_demo/split 0.006s
现在我们回过头来解决我们程序中的问题。很显然我们最初的split
函数并没有考虑到sep为多个字符的情况,我们来修复下这个Bug:
package split
import "strings"
// split package with a single split function.
// Split slices s into all substrings separated by sep and
// returns a slice of the substrings between those separators.
func Split(s, sep string) (result []string) {
i := strings.Index(s, sep)
for i > -1 {
result = append(result, s[:i])
s = s[i+len(sep):] // 这里使用len(sep)获取sep的长度
i = strings.Index(s, sep)
}
result = append(result, s)
return
}
这一次我们再来测试一下,我们的程序。注意,当我们修改了我们的代码之后不要仅仅执行那些失败的测试函数,我们应该完整的运行所有的测试,保证不会因为修改代码而引入了新的问题。
split $ go test -v
=== RUN TestSplit
--- PASS: TestSplit (0.00s)
=== RUN TestMoreSplit
--- PASS: TestMoreSplit (0.00s)
PASS
ok github.com/Q1mi/studygo/code_demo/test_demo/split 0.006s
这一次我们的测试都通过了。
我们现在还想要测试一下split
函数对中文字符串的支持,这个时候我们可以再编写一个TestChineseSplit
测试函数,但是我们也可以使用如下更友好的一种方式来添加更多的测试用例。
func TestSplit(t *testing.T) {
type test []struct {
input string
sep string
want []string
}
tests := test{
{input: "a:b:c", sep: ":", want: []string{"a", "b", "c"}},
{input: "a:b:c", sep: ",", want: []string{"a:b:c"}},
{input: "abcd", sep: "bc", want: []string{"a", "d"}},
{input: "沙河有沙又有河", sep: "沙", want: []string{"河有", "又有河"}},
}
for _, tc := range tests {
got := Split(tc.input, tc.sep)
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("excepted:%v, got:%v", tc.want, got)
}
}
}
我们通过上面的代码把多个测试用例合到一起,再次执行go test
命令。
split $ go test -v
=== RUN TestSplit
--- FAIL: TestSplit (0.00s)
split_test.go:42: excepted:[河有 又有河], got:[ 河有 又有河]
FAIL
exit status 1
FAIL github.com/Q1mi/studygo/code_demo/test_demo/split 0.006s
我们的测试出现了问题,仔细看打印的测试失败提示信息:excepted:[河有 又有河], got:[ 河有 又有河]
,你会发现[ 河有 又有河]
中有个不明显的空串,这种情况下十分推荐使用%#v
的格式化方式。
我们修改下测试用例的格式化输出错误提示部分:
func TestSplit(t *testing.T) {
...
for _, tc := range tests {
got := Split(tc.input, tc.sep)
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("excepted:%#v, got:%#v", tc.want, got)
}
}
}
此时运行go test
命令后就能看到比较明显的提示信息了:
split $ go test -v
=== RUN TestSplit
--- FAIL: TestSplit (0.00s)
split_test.go:42: excepted:[]string{"河有", "又有河"}, got:[]string{"", "河有", "又有河"}
FAIL
exit status 1
FAIL github.com/Q1mi/studygo/code_demo/test_demo/split 0.006s
看起来都挺不错的了,但是你有没有想过当我们测试用例比较多的时候,我们是没办法一眼看出来具体是哪个测试用例失败了。我们可能会想到下面的解决办法:
func TestSplit(t *testing.T) {
type test struct { // 定义test结构体
input string
sep string
want []string
}
tests := map[string]test{ // 测试用例使用map存储
"simple": {input: "a:b:c", sep: ":", want: []string{"a", "b", "c"}},
"wrong sep": {input: "a:b:c", sep: ",", want: []string{"a:b:c"}},
"more sep": {input: "abcd", sep: "bc", want: []string{"a", "d"}},
"leading sep": {input: "沙河有沙又有河", sep: "沙", want: []string{"河有", "又有河"}},
}
for name, tc := range tests {
got := Split(tc.input, tc.sep)
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("name:%s excepted:%#v, got:%#v", name, tc.want, got) // 将测试用例的name格式化输出
}
}
}
这样做是能够解决问题的。同时Go1.7+新增了子测试,我们可以按照如下方式使用:
func TestSplit(t *testing.T) {
type test struct { // 定义test结构体
input string
sep string
want []string
}
tests := map[string]test{ // 测试用例使用map存储
"simple": {input: "a:b:c", sep: ":", want: []string{"a", "b", "c"}},
"wrong sep": {input: "a:b:c", sep: ",", want: []string{"a:b:c"}},
"more sep": {input: "abcd", sep: "bc", want: []string{"a", "d"}},
"leading sep": {input: "沙河有沙又有河", sep: "沙", want: []string{"河有", "又有河"}},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) { // 使用t.Run()执行子测试
got := Split(tc.input, tc.sep)
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("excepted:%#v, got:%#v", tc.want, got)
}
})
}
}
此时我们再执行go test
命令就能够看到更清晰的输出内容了:
split $ go test -v
=== RUN TestSplit
=== RUN TestSplit/leading_sep
=== RUN TestSplit/simple
=== RUN TestSplit/wrong_sep
=== RUN TestSplit/more_sep
--- FAIL: TestSplit (0.00s)
--- FAIL: TestSplit/leading_sep (0.00s)
split_test.go:83: excepted:[]string{"河有", "又有河"}, got:[]string{"", "河有", "又有河"}
--- PASS: TestSplit/simple (0.00s)
--- PASS: TestSplit/wrong_sep (0.00s)
--- PASS: TestSplit/more_sep (0.00s)
FAIL
exit status 1
FAIL github.com/Q1mi/studygo/code_demo/test_demo/split 0.006s
这个时候我们要把测试用例中的错误修改回来:
func TestSplit(t *testing.T) {
...
tests := map[string]test{ // 测试用例使用map存储
"simple": {input: "a:b:c", sep: ":", want: []string{"a", "b", "c"}},
"wrong sep": {input: "a:b:c", sep: ",", want: []string{"a:b:c"}},
"more sep": {input: "abcd", sep: "bc", want: []string{"a", "d"}},
"leading sep": {input: "沙河有沙又有河", sep: "沙", want: []string{"", "河有", "又有河"}},
}
...
}
测试覆盖率
由测试代码触发运行到的被检测函数的代码数目被称为测试的覆盖率。通常我们使用的都是语句的覆盖率,也就是在测试中至少被运行一次的代码占总代码的比例。