测试覆盖率是一个术语,用于统计通过运行程序包的测试多少代码失去执行。如果执行测试套件导致 80%的语句失去了运行,则测试覆盖率为 80%。
计算测试覆盖率的通常办法是埋点二进制可执行文件。例如,GNU gcov 在二进制文件中设置执行分支断点。当每个分支执行时,断点被革除,并且分支的指标语句被标记为“被笼罩”。
这种办法是胜利和宽泛应用的。Go 的晚期测试笼罩工具甚至以雷同的形式工作。但它有问题。因为剖析二进制文件的执行是很艰难的,所以很难实现。它还须要将执行跟踪绑定回源代码的牢靠办法,这也可能是艰难的。那里的问题包含不正确的调试信息和诸如内联性能的问题, 使剖析变得复杂。最重要的是,这种办法十分不具备可移植性。对于每个机器架构须要从新编写,在某种程度上, 可能对于每个操作系统都须要从新编写,因为从零碎到零碎的调试反对差别很大。
Go 1.2 的公布引入了一个 test coverage 的新工具,它采纳了一种不寻常的形式来生成覆盖率统计数据,这种办法建设在 Godoc 的技术的根底上。
1 Go 的测试覆盖率
对于 Go 的新测试笼罩工具,采取了一种防止动静调试的不同办法。想法很简略:在编译之前重写包的源代码,以埋点,编译和运行批改的源,并转储统计信息。重写很容易编排,因为 go 的工具链 管制从源到测试到执行的整个流程。
示例代码如下:
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"
}
测试代码如下:
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)
}
}
}
执行代码覆盖率测试如下:
cd ../src/cover/size/
go test ./... -cover
cd -
PASS
coverage: 42.9% of statements
ok _/home/parallels/program/org/github-pages/source/src/cover/size 0.001s
/home/parallels/program/org/github-pages/source/_posts
启用测试笼罩后,/go test/ 运行 cover 工具,在编译之前重写源代码。以下是重写后的 Size 函数:
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 工具生成的第二个只读数据结构记录的语句的原始源地位相关联。测试运行实现后,收集计数器,通过查看设置的数量的来计算百分比。
尽管调配注解看起来可能很低廉,然而它被编译为单个“挪动”指令。因而,其运行时开销不大,运行典型(或更理论)测试时只减少约 3%开销。这使得把测试覆盖率作为规范开发流程的一部分是荒诞不经的。
2 查看后果
下面的例子的测试覆盖率很差。为了摸索具体为什么,须要 go test 写一个 coverage profile,这是一个保留收集的统计信息的文件,以便能具体地钻研笼罩的细节。这很容易做:应用 -coverprofile 标记来指定输入的文件:
cd ../src/cover/size/
go test -coverprofile=size_coverage.out
注:-coverprofile 标记主动设置 -cover 来启用覆盖率剖析。
测试与以前一样运行,但后果保留在文件中。要钻研它们,须要运行 test coverage tool。一开始,能够要求 覆盖率 按函数合成,尽管在当前情况下没有太多意义,因为只有一个函数:
cd ../src/cover/size/
go tool cover -func=size_coverage.out
查看的更乏味的形式是获取 覆盖率信息正文的源代码 的 HTML 展现。该显示由 -html 标记调用:
cd ../src/cover/size/
go tool cover -html=size_coverage.out
运行此命令时,浏览器将弹出窗口,已笼罩(绿色),未笼罩(红色)和 未埋点(灰色)。上面是一个屏幕截图:
<img src="/images/go-test-cover-set.png"/>
有了这个信息页,问题变得很显著:下面疏忽了几个 case 的测试!能够精确地看出具体是哪一个,这样能够轻松地进步的测试覆盖率。
3 热力求
源代码级形式来测试覆盖率的一大长处在于,能够很容易用不同的形式对代码进行埋点解决。例如,不仅能够检测是否已执行了一个语句,而且还能够查问执行了多少次。
go test 命令承受 -covermode 标记将笼罩模式设置为三种设置之一:
- set: 每个语句是否执行?
- count: 每个语句执行了几次?
- atomic: 相似于 count, 但示意的是并行程序中的准确计数
set 是默认设置,下面示例曾经看到了。只有运行并行算法须要准确的计数时,才须要进行 atomic 设置。它应用来自 sync/atomic 包的原子操作,这可能会相当低廉。然而,对于大多数状况,count 模式工作失常,并且像默认设置模式一样十分快。
上面来试试一个规范包,fmt 格式化包语句执行的计数。进行测试并写出 coverage profile,以便可能很好地进行信息的出现。
go test -covermode=count -coverprofile=../src/cover/count.out fmt
这比以前的例子好的测试覆盖率。(覆盖率不受笼罩模式的影响)能够显示函数细节:
go tool cover -func=../src/cover/count.out
HTML 输入产生了微小的回报:
go tool cover -html=../src/cover/count.out
pad 函数如下所示:
<img src="/images/go-test-cover-count.png"/>
留神绿色的强度是如何变动。最亮堂的绿色的代表较高的执行数; 较少灰暗的绿色代表较低的执行数。甚至能够将鼠标悬停在语句上,以便在弹出的 tool tip 中提醒理论计数。test coverage 产生了对于函数执行的大量信息,在剖析中很有用的信息。
4 根底块
你可能曾经留神到,上一个示例中 / 有对于闭合大括号两头的行的计数 / 不是你所冀望的那样。这是因为始终以来 test coverage 都不是一个不准确的迷信。
这里产生的很值得解释。咱们心愿笼罩注解由程序中的分支划分,当二进制文件在传统办法中被调用时,它们是离开的。不过,通过重写源代码很难做到这一点,因为分支没有明确展现在源代码中。
笼罩注解的作用是是埋点,通常由大括号来限定。一般来说,使之工作失常是十分艰难的。所应用的算法的处理结果是闭合括号看起来像属于它配对的块,而凋谢大括号看起来像属于块之外。一个更乏味的后果呈现在如下的一个表达式中:
f() && g()
没有试图独自调用对 f 和 g 的调用,无论事实如何,它们总是看起来像是运行雷同的次数。
偏心来说,即便 gcov 在这里也有麻烦。该工具使机制正确,但出现是基于行的,因而可能会错过一些细微差别。
5 总结
这是对于 Go 1.2 test coverage 故事。具备乏味实现的新工具不仅能够实现测试覆盖率的统计,而且易于解释,甚至能够提取 profile 信息。
测试是软件开发和的重要组成部分,/test coverage/ 为测试策略增加一个简略的规范。走向前,test 和 cover。