Golang测试覆盖率

Golang测试覆盖率

最近实习在做一些CI/CD的工作,用到了go的一些测试工具,学习记录一下。都是官网博客找的来源:https://blog.golang.org/cover

golang1.2引进了一种新的测试覆盖工具,采用一种特别的方法来生成覆盖率统计数据。

覆盖率测试

代码覆盖率测试是指,通过运行一个测试来查看有多少代码被执行了。如果使用的测试样例导致80%的源代码语句运行,我们就认为代码覆盖率为80%。

go语言引进了一个轻量级的测试框架testing和自带的go test命令来实现单元测试和性能测试。这套框架的原理很简单,就是在2变异之前重写package的源代码以添加检测、编译和运行修改后的源代码,并且将统计信息转移。

golang的覆盖率测试

假设有这样一个源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package size

func Size(a int) string {
switch {
case a < 0:
return "negative"
case a == 0:
return "zero"
case a < 10:
return "small"
case a < 100:
return "big"
case a < 1000:
return "huge"
}
return "enormous"
}

和这样的一个测试样例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package size

import "testing"

type Test struct {
in int
out string
}

var tests = []Test{
{-1, "negative"},
{5, "small"},
}

func TestSize(t *testing.T) {
for i, test := range tests {
size := Size(test.in)
if size != test.out {
t.Errorf("#%d: Size(%d)=%s; want %s", i, test.in, size, test.out)
}
}
}

然后使用go test命令加上-cover选项来测试代码覆盖率,产生结果如下:

1
2
3
PASS
coverage: 42.9% of statements
ok _/Users/lucien/Study/gosrc 0.006s
  • 由于go test命令只能在一个相应的目录下执行所有文件,注意这两个文件需要在同一个目录下。
  • 另外测试文件必须要以_test.go结尾,这样在执行go test的时候才能执行到相应的代码。
  • 在测试文件中必须要import testing这个包,另外所有测试函数都以Test开头,并且参数是testing.T,用来记录错误或者测试状态;格式为:
1
func TestXxx (t *testing.T) //Xxx开头必须大写

上面的结果显示,我们代码的测试覆盖率为42.9%,这个数字怎么来的呢?当启用测试覆盖的时候,go test会用cover工具在编译之前重写源代码,在原始代码中寻找分支,然后在每个分支"种下"锚点。 等所有的case都跑完后,通过统计执行锚点的数量来计算覆盖率。比如变成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func Size(a int) string {
GoCover.Count[0] = 1
switch {
case a < 0:
GoCover.Count[2] = 1
return "negative"
case a == 0:
GoCover.Count[3] = 1
return "zero"
case a < 10:
GoCover.Count[4] = 1
return "small"
case a < 100:
GoCover.Count[5] = 1
return "big"
case a < 1000:
GoCover.Count[6] = 1
return "huge"
}
GoCover.Count[1] = 1
return "enormous"
}

虽然这样重写代码的方式开销看起来比较昂贵,但实际上它会被编译为单个的move指令,在实际测试时开销仅仅增加3%。

Although that annotating assignment might look expensive, it compiles to a single "move" instruction. Its run-time overhead is therefore modest, adding only about 3% when running a typical (more realistic) test. That makes it reasonable to include test coverage as part of the standard development pipeline.

查看结果

实际上,刚刚那种测试结果看起来比较简陋,我们需要了解更加详细的测试内容,因此我们可以使用go test来输出一个coverage profile,该文件保存着收集的统计信息的文件,使得我们研究它们更加方便。

1
2
3
4
5
% go test -coverprofile=coverage.out 
PASS
coverage: 42.9% of statements
ok size 0.030s
%

输出结果为:

1
2
3
4
5
6
7
8
mode: set
_/Users/lucien/Study/gosrc/test.go:3.25,4.12 1 1
_/Users/lucien/Study/gosrc/test.go:16.5,16.22 1 0
_/Users/lucien/Study/gosrc/test.go:5.16,6.26 1 1
_/Users/lucien/Study/gosrc/test.go:7.17,8.22 1 0
_/Users/lucien/Study/gosrc/test.go:9.17,10.23 1 1
_/Users/lucien/Study/gosrc/test.go:11.18,12.21 1 0
_/Users/lucien/Study/gosrc/test.go:13.19,14.22 1 0

有了这么一个文件,我们可以不进行测试,直接查看测试结果:

1
2
3
4
% go tool cover -func=coverage.out
size.go: Size 42.9%
total: (statements) 42.9%
%

热图

我们可以以多种方式来检测代码覆盖率,go test接受-covermode标志来将覆盖模式设置为以下三种:

  • set:每个语句都运行了吗
  • count:每个语句运行了多少次
  • atomic:跟count一样,但能平行程序中精确计算(使用了来自sync/atomic的原子操作)

运行这样一行命令,会打开一个浏览器,注意查看绿色的强度如何变化。更明亮的绿色表示具有更高的执行次数。将鼠标停留在语句上,会显示具体的执行次数

1
go tool cover -html=count.out