不写测试的开发不是好程序员!
原文链接
测试工具
测试命令
在Go语言中,使用单元测试的主要工具命令就是
go test
,不需要额外其他工具或库来进行单元测试,在使用
go test
命令时,系统会自动根据一定的约定将当前包内的所有以
_test.go
为后缀的源代码文件作为测试文件,同时测试文件中所有以
Test
为前缀的函数都将被作为测试函数全部执行。
文件命名
基于上面的描述,在写单元测试代码时,需要遵循一定的规范:
- 通常单元测试文件和源代码程序文件放在一起,不使用额外的
test
包; - 单元测试文件必须以
_test.go
为后缀; - 单元测试函数必须以
Test
为前缀(基准测试函数以Benchmark
为前缀); - 单元测试函数参数必须为
t *testing.T
(基准测试函数为b *testing.B
);
单元测试
根据上面的规则,在使用单元测试时,必须导入
testing
包,函数名称必须以
Test
为前缀,后缀必须以大写字母开头,通常后缀为对应需要测试的函数名称,以下示例为测试函数的基本格式:
// Add函数对应单元测试函数funcTestAdd(t *testing.T){}
基本使用
现在,我们写一个简单的计算绝对值的函数
Abs
,然后通过这个函数来看单元测试的基本使用函数,首先定义一个包,在包里面新建源代码文件和测试文件,目录结构如下:
calc
--calc.go--calc_test.go
在
calc.go
源代码文件中写一个简单的函数用于计算绝对值。
funcAbs(i int)int{if i >=0{return i
}else{return-i
}}
然后在
calc_test.go
测试文件中写一个简单的单元测试函数。
funcTestAbs(t *testing.T){
got :=Abs(-1)if got !=1{
t.Errorf("excepted: %d, got: %d",1, got)}}
代码中调用
Abs
函数,并传入参数
-1
,使用
got
接收返回值,如果返回值不是我们期望的
1
,则可以使用
t.Errorf
输出错误信息。
然后使用
go test
命令运行单元测试:
PS C:\Users\lee\GolandProjects\test\test>go test
PASS
ok test/calc 0.200s
可以看到输出了
PASS
表示测试通过,同时还可以在命令后面加上
-v
查看更详细输出
PS C:\Users\lee\GolandProjects\test\calc>go test -v
===RUN TestAbs
--- PASS: TestAbs (0.00s)
PASS
ok test/calc 0.142s
记得我们上面说过的,如果使用
go test
命令系统会自动根据一定的约定将当前包内的所有以
_test.go
为后缀的源代码文件作为测试文件,同时测试文件中所有以
Test
为前缀的函数都将被作为测试函数全部执行。但如果我们仅仅只需要跑其中一个测试函数怎么办呢,可以在命令后面加上
-run
参数指定函数名称,即可运行指定的测试函数。
首先在源代码文件和测试文件中分别加入一个功能函数和一个测试函数。
funcAdd(i, j int)int{return i + j
}
funcTestAdd(t *testing.T){
got :=Add(1,2)if got !=3{
t.Errorf("excepted: %d, got: %d",1, got)}}
这时候如果直接执行
go test
就会将两个测试函数都执行,通过指定的方式只执行其中一个测试函数。
PS C:\Users\lee\GolandProjects\test\calc>go test -v -run=Add
===RUN TestAdd
--- PASS: TestAdd (0.00s)
PASS
ok test/calc 0.151s
在通过
-run
命令指定函数的时候可以写功能函数的名称,也可以写测试函数的名称,两者都是一样的效果。
测试覆盖率
go test
命令还有一个参数是用来专门看单元测试对于功能代码的覆盖情况,命令为
go test -cover
,使用该命令可以清晰的看到单元测试的覆盖率。
PS C:\Users\lee\GolandProjects\test\calc>go test -cover
PASS
coverage:75.0% of statements
ok test/calc 0.188s
使用该命令针对我们上面的单元测试进行测试,能够看到单元测试的覆盖率为
75%
,这表示在功能性代码里面还有一部分的情况没有测试到,这样的话我们可以使用
-coverprofile
参数将测试的覆盖率保存到文件中,然后在文件中查看到底是哪部分的内容没有被测试到的。
C:\Users\lee\GolandProjects\test\calc>go test -cover -coverprofile=cover.out
PASS
coverage:75.0% of statements
ok test/calc 0.184s
使用该命令之后就会在当前目录下生成一个
cover.out
文件(注意:使用该命令需要确保当前命令行有管理员权限,否则会导致创建文件失败),用来保存测试覆盖率相关的记录,但是我们打开当前文件发现里面的内容并不能看出是哪个地方没有被覆盖到,这时候我们可以使用
go tool
命令来将记录文件生成一个
HTML
格式的报告。
go tool cover -html=cover.out
直接运行这个命令就会生成
HTML
的报告,并直接在浏览器中打开,报告如下:
通过报告就能看到是
Abs
函数中关于负数部分的分支没有被覆盖到,这时候如果我们在
TestAbs
函数中增加一个测试用例测试负数的情况,再跑覆盖率就会达到
100%
的覆盖率。
测试组
正如上面所讲,单元测试需要尽可能多的覆盖功能代码中的不同情况,这个时候可能就需要在测试代码中写上很多的测试用例,但是一个个去写测试用例肯定不是一个明确的做法,所以这个时候就需要使用测试组,测试组简单理解就是将多个测试用例汇聚到一起,然后使用循环的方式来一个个测试,如果需要新增或删除测试用例可以直接在数组中修改,不需要改动代码。测试组的使用方式如下:
funcTestAbs(t *testing.T){// 定义测试用例需要使用到的结构体,包含输入以及期望的结果type test struct{
input int
want int}// 构建测试用例
tests :=[]test{{input:1, want:1},{input:2, want:2},{input:0, want:0},{input:-1, want:1},}// 循环测试组对每个测试用例进行测试for_, tc :=range tests {
got :=Abs(tc.input)if got != tc.want {
t.Errorf("want: %d, got: %d", tc.want, got)}}}
使用命令进行测试:
PS C:\Users\lee\GolandProjects\test\calc>go test -v -run=Abs===RUN TestAbs
--- PASS: TestAbs (0.00s)
PASS
ok test/calc 0.163s
通过测试组可以将测试多个测试用例聚合到一起进行测试,方便测试用例的新增和删除,这样不仅代码美观还提高了代码的扩展性。
子测试
通过上面测试组的方法,能够很容易的将多个测试用例汇聚到一起,但同时也带来了一个问题,那就是当测试用例过多的时候,如果其中某个测试用例发生错误,不太容易能够看出是哪个测试用例的问题,这时候我们就需要给所有的测试用例一个特定的标签或名字,代码修改如下,并将其中的某一个用例的期望值改成错误的。
funcTestAbs(t *testing.T){// 定义测试用例需要使用到的结构体,包含输入以及期望的结果type test struct{
input int
want int}// 构建测试用例
tests :=map[string]test{"No1":{input:1, want:1},"No2":{input:2, want:3},"No3":{input:0, want:0},"No4":{input:-1, want:1},}// 循环测试组对每个测试用例进行测试for name, tc :=range tests {
got :=Abs(tc.input)if got != tc.want {
t.Errorf("name: %s, want: %d, got: %d", name, tc.want, got)}}}
然后开始测试
PS C:\Users\lee\GolandProjects\test\calc>go test -v -run=Abs===RUN TestAbs
calc_test.go:35:name: No2, want:3, got:2--- FAIL: TestAbs (0.00s)
FAIL
exitstatus1
FAIL test/calc 0.196s
通过上面的输出就能够看到是名称为
No2
的测试用例没有通过,使用上面的方法虽然可以简单的看到出错的测试用例,但是还不够明显,这时候可以使用单元测试中的子测试,通过子测试方法能够更清晰明了的看到每一个用例的通过情况,修改代码如下:
funcTestAbs(t *testing.T){// 定义测试用例需要使用到的结构体,包含输入以及期望的结果type test struct{
input int
want int}// 构建测试用例
tests :=map[string]test{"No1":{input:1, want:1},"No2":{input:2, want:3},"No3":{input:0, want:0},"No4":{input:-1, want:1},}// 循环测试组对每个测试用例进行测试for name, tc :=range tests {
t.Run(name,func(t *testing.T){
got :=Abs(tc.input)if got != tc.want {
t.Errorf("want: %d, got: %d", tc.want, got)}})}}
测试函数的参数
t
可以调用
Run
方法,该方法接收一个
name
参数和一个函数类型的参数,然后再次开始测试
PS C:\Users\lee\GolandProjects\test\calc>go test -v -run=Abs===RUN TestAbs
===RUN TestAbs/No3
===RUN TestAbs/No4
===RUN TestAbs/No1
===RUN TestAbs/No2
calc_test.go:43: want:3, got:2--- FAIL: TestAbs (0.00s)--- PASS: TestAbs/No3 (0.00s)--- PASS: TestAbs/No4 (0.00s)--- PASS: TestAbs/No1 (0.00s)--- FAIL: TestAbs/No2 (0.00s)
FAIL
exitstatus1
FAIL test/calc 0.182s
通过上面的输出能够清晰的看到通过的测试用例以及没有通过的测试用例,相比较于第一种的方式,输出更加清晰明了,这就是使用子测试的好处。
下一期分享go语言中的基准测试,包含基本使用、性能测试、并发测试,欢迎关注。
版权归原作者 CodeJR 所有, 如有侵权,请联系我们删除。