Go 1.2 高亮:测试代码覆盖率
罗伯·派克
2013年12月2日
介绍
从项目的开始,Go的设计就考虑了工具。这些工具包括一些最具标志性的Go技术,例如文档演示工具godoc,代码格式化工具gofmt和API重写器gofix。也许最重要的是go
命令,该程序仅使用源代码即可自动安装,构建和测试Go程序。作为构建规范。
Go 1.2的发行版引入了一种新的测试覆盖率工具,它采用了一种不寻常的方法来生成覆盖率统计信息,该方法基于godoc和朋友们建立的技术。
####支持工具
首先,有一些背景知识:支持良好工具的语言是什么意思?这意味着该语言可以轻松编写出色的工具,并且其生态系统支持各种口味的工具的构建。
Go的许多属性使其适合用于工具。对于初学者来说,Go具有易于解析的常规语法。语法旨在避免需要复杂机制进行分析的特殊情况。
在可能的情况下,Go使用词汇和句法构造使语义属性易于理解。例如,与C传统中的其他语言相比,使用大写字母定义导出的名称以及从根本上简化了作用域规则。
最后,标准库附带了生产质量的软件包,用于lex和解析Go源代码。它们还包含(通常情况下)生产质量好的软件包,用于精美打印Go语法树。
这些软件包组合在一起构成了gofmt工具的核心,但是漂亮的打印机值得一提。因为它可以采用任意的Go语法树并输出标准格式的,人类可读的正确代码,所以它有可能构建工具来转换解析树并输出经过修改但正确易读的代码。
一个示例是gofix工具,它可以自动重写代码以使用新的语言功能或更新的库。 Gofix让我们在Run to Go 1.0中对语言和库进行了根本性的更改,并相信用户可以运行该工具以将其源更新为最新版本。
在Google内部,我们使用gofix在巨大的代码存储库中进行了大范围的更改,而这在我们使用的其他语言中几乎是无法想象的。不再需要支持某个API的多个版本;我们可以使用gofix在一次操作中更新整个公司。
当然,这些软件包启用的不仅仅是这些大型工具。例如,它们还使编写更简单的程序(例如IDE插件)变得容易。所有这些项目都是相互构建的,通过自动化许多任务使Go环境的生产率更高。
####测试覆盖率
测试覆盖率是一个术语,描述了通过运行程序包测试来行使多少程序包代码。如果执行测试套件导致运行包的80%的源语句,那么我们说测试覆盖率为80%。
Go 1.2中提供测试覆盖率的程序是最新利用Go生态系统中工具支持的程序。
计算测试覆盖率的通常方法是检测二进制文件。例如,GNU gcov程序在由二进制文件执行的分支上设置断点。在每个分支执行时,将清除断点并将分支的目标语句标记为“已覆盖”。
这种方法是成功的并且被广泛使用。 Go的早期测试覆盖率工具甚至可以以相同的方式工作。但这有问题。由于对二进制执行的分析具有挑战性,因此难以实施。它还需要一种可靠的方式来将执行跟踪绑定到源代码,这也很困难,因为源级调试器的任何用户都可以证明。那里的问题包括错误的调试信息以及诸如内联函数等使分析复杂化的问题。最重要的是,这种方法是非常不可移植的。由于调试支持在系统之间存在很大差异,因此对于每种体系结构都需要重新完成,并且在某种程度上需要对每个操作系统重新进行。
不过,它确实有效,例如,如果您是gccgo的用户,则gcov工具可以为您提供测试覆盖率信息。但是,如果您是gc(更常用的Go编译器套件)的用户,那么在Go 1.2之前,您还是很不走运。
Go的测试范围
对于Go的新测试覆盖率工具,我们采用了另一种避免动态调试的方法。这个想法很简单:在编译之前重写包的源代码以添加检测,编译和运行修改后的源以及转储统计信息。重写很容易安排,因为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
%
请注意,覆盖率为42.9%,不是很好。在询问如何增加该数字之前,让我们看看如何计算该数字。
启用测试覆盖率后,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"
}
程序的每个可执行节都带有一个赋值语句,该赋值语句在执行时记录该节的运行。计数器通过第二个只读数据结构(它也由封面工具生成)与它计数的语句的原始源位置相关联。测试运行完成后,将收集计数器并通过查看设置了多少来计算百分比。
尽管该注释分配可能看起来很昂贵,但它可以编译为一条“移动”指令。因此,它的运行时开销是适度的,在运行典型的(更现实的)测试时仅增加了约3%。这使得将测试覆盖率纳入标准开发流程的一部分是合理的。
查看结果
本例的测试覆盖率很差. 为了找到原因, 我们要求 go
test
为我们写一个 “覆盖配置文件”, 该文件包含收集的统计信息, 以便我们可以更详细地研究它们. 这很容易做到: 使用 -coverprofile
标志为输出指定文件:
% go test -coverprofile=coverage.out
PASS
coverage: 42.9% of statements
ok size 0.030s
%
(-coverprofile
标志会自动设置 -cover
以启用覆盖率分析.) 测试与以前一样运行, 但是结果保存在文件中. 为了研究它们, 我们自己运行了测试覆盖率工具, 而没有 go
test
. 首先, 我们可以要求按功能细分覆盖范围, 尽管在这种情况下, 由于只有一个功能, 所以并不能说明什么:
% go tool cover -func=coverage.out
size.go: Size 42.9%
total: (statements) 42.9%
%
查看数据的一种更有趣的方法是获取以覆盖率信息修饰的源代码的 HTML 表示. 此显示由 -html
标志调用:
$ go tool cover -html=coverage.out
运行此命令时, 将弹出一个浏览器窗口, 显示覆盖的 (绿色), 未覆盖的 (红色) 和未显示的 (灰色) 源. 这是一个屏幕转储:
通过此演示, 可以很明显看出什么问题: 我们忽略了对几个案例的测试! 而且我们可以准确地看到它们是哪一个, 这可以轻松地提高我们的测试覆盖率.
热力图
这种用于测试覆盖率的源代码级方法的一大优势在于, 很容易以不同的方式来检测代码. 例如, 我们不仅可以询问一条语句是否已执行, 还可以询问多少次.
go
test
命令接受 -covermode
标志以将 coverage 模式设置为以下三种设置之一:
- set: 每个语句是否运行?
- count: 每个语句运行了多少次?
- atomic: 与 count 类似, 但能并行程序中精确计数
默认值为 “set”, 我们已经看到了. 仅在运行并行算法时需要精确计数时才需要 atomic
设置. 它使用 sync/atomic 软件包中的原子操作, 这可能会非常昂贵. 但是, 对于大多数用途而言, count
模式可以正常工作, 并且像默认的 set
模式一样非常廉价易用.
让我们尝试计算标准包中的 fmt
格式包的语句执行情况. 我们运行测试并写出覆盖率配置文件, 以便以后可以很好地呈现信息.
% go test -covermode=count -coverprofile=count.out fmt
ok fmt 0.056s coverage: 91.7% of statements
%
与我们之前的示例相比, 这具有更好的测试覆盖率. (覆盖率不受覆盖模式的影响.) 我们可以显示功能细分:
% go tool cover -func=count.out
fmt/format.go: init 100.0%
fmt/format.go: clearflags 100.0%
fmt/format.go: init 100.0%
fmt/format.go: computePadding 84.6%
fmt/format.go: writePadding 100.0%
fmt/format.go: pad 100.0%
...
fmt/scan.go: advance 96.2%
fmt/scan.go: doScanf 96.8%
total: (statements) 91.7%
最大的收获发生在 HTML 输出中:
% go tool cover -html=count.out
这是演示文稿中的 pad
函数的结果:
注意绿色强度的变化. 绿色的语句具有较高的执行次数; 饱和度较低的绿色表示较低的执行计数. 您甚至可以将鼠标悬停在这些语句上, 以查看实际计数在工具提示中弹出. 在撰写本文时, 计数是这样显示的 (我们已将计数从工具提示移至行首标记, 以使其更易于显示):
2933 if !f.widPresent || f.wid == 0 {
2985 f.buf.Write(b)
2985 return
2985 }
56 padding, left, right := f.computePadding(len(b))
56 if left > 0 {
37 f.writePadding(left, padding)
37 }
56 f.buf.Write(b)
56 if right > 0 {
13 f.writePadding(right, padding)
13 }
关于函数执行的很多信息, 这些信息可能对性能分析很有用.
####基本块
您可能已经注意到,上一个示例中的计数不是用大括号括起来的行所期望的。这是因为与往常一样,测试覆盖率是一门不精确的科学。
不过,这里发生的事情值得解释。我们希望coverage注释由程序中的分支来划分,就像在传统方法中检测二进制文件时那样。但是,通过重写源代码很难做到这一点,因为分支未在源代码中明确显示。
覆盖注释的作用是仪表块,通常由括号括起来。通常很难做到这一点。使用该算法的结果是,闭合括号看起来像它属于它所闭合的块,而闭合括号看起来像它属于该块外部。一个更有趣的结果是,在类似
f()&& g()
并没有尝试分别检测对f和g的调用,无论事实如何,看起来它们总是运行相同的次数,即f的运行次数。
公平地说,即使gcov在这里也有麻烦。该工具可以正确使用仪器,但是演示文稿是基于行的,因此可能会遗漏一些细微差别。
####大的愿景
这就是关于Go 1.2中测试覆盖率的故事。一种具有有趣实现方式的新工具不仅可以测试覆盖率统计信息,还可以轻松解释它们的表示形式,甚至可以提取配置文件信息。
测试是软件开发和测试覆盖范围的重要组成部分,这是一种将纪律添加到测试策略中的简单方法。继续进行测试并掩盖。
本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
推荐文章: