今天讲一下go语言的test coverage是如何实现的。
上篇讲到了go语言1.2之前对于测试覆盖的支持,使用了比较“传统”的做法,也就是对于二进制文件的analysis和instrument。
从go v1.2开始,新的测试覆盖工具使用了完全不同的方法。思路非常简单:在编译之前重写源文件,在源文件中加入一些instrument,然后编译和执行被修改的源文件,得到覆盖的统计信息。重写源文件其实不难,得益于go强大的命令行工具 - 它负责从源文件编译,到执行测试,直到程序的执行全盘搞定 - 所以在这个全过程的工具中“重写”代码不在话下。
一起看看下面的简单例子:
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"
}
以及单元测试代码:
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参数就可以得到覆盖率的统计数据。
% go test -cover
PASS
coverage: 42.9% of statements
ok size 0.026s
%
接下来我们看看这个统计数据是怎么计算得出的。当带上-cover参数之后,go test会运行一个独立的程序,名字就叫cover,它会在编译之前改写源代码。
改写之后的代码差不多长这个样子:
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"
}
注意代码中新加入的行,它们分布在代码的每个分支上,当对应的分支被执行到的时候它们就会做对应的记录。它们看起来像是计数器,每个计数器都会记录对应在代码中的原始位置。这些重要的对应信息都是由"cover"程序生成的。当测试程序运行完毕后,计数器会被收集起来,计算覆盖的结果。虽然新加入的那些行看着代价挺高,其实不然,因为都是简单的赋值操作,它们被编译成"MOV"指令,所以它们的运行时开销也不大,通常的影响是大约3%。这对于日常的开发不会造成负面影响。
查看覆盖率结果
go test命令能够将覆盖率结果输出到文件中,以便我们查看是哪些代码行没有被测试覆盖到。
% go test -coverprofile=coverage.out
PASS
coverage: 42.9% of statements
ok size 0.030s
%
参数 -coverprofile会自动设置
-cover参数,启用覆盖率分析。我们可以用如下命令查看结果文件
:
% go tool cover -func=coverage.out
size.go: Size 42.9%
total: (statements) 42.9%
%
当然,一种更加让人喜闻乐见的格式是HTML。
$ go tool cover -html=coverage.out
执行这个命令会打开一个浏览器,用图形化的方式显示覆盖的细节。
如果觉得这篇文章对你有用,点击关注,或者留言讨论,一起在学习的大道上超越昨天的自己。