go 覆盖测试工具介绍
go 覆盖测试工具介绍
原文链接 the cover story
introduction
Go 语言内置了许多工具,比如godoc (可以根据你在注释中的内容生成介绍文档,注释格式参照,godoc -http=:6060 -play
运行一个playground),还有 gofmt
(自动格式化代码),还有gofix(自动根据go的版本更新你的调用函数)。
test coverage
通常的覆盖测试的方法是构造二进制文件。比如 gcov 程序在程序的每个分支设置断点,一旦这个分支被执行,断点被清除,目标分支被标记为“已覆盖”。
这种方式很成功而且被广泛运用,但是它的问题在于难以实现,更重要的是,它是不可移植的。不同的架构,甚至不同的系统下都需要重新设置,因为它们的调试方法各不相同。
Test coverage for Go
在Go的新的覆盖测试方法中使用了一种新的方式来避免动态调试。方法很简单:那就是在编译之前重写代码,然后编译运行,打印统计信息。重写代码非常简单因为 go 命令可以控制整个 “代码->测试->执行” 流程。
让我们来运行一个简单的例子,创建文件夹 test_cover,在其中创建文件:
// size.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"
}
测试文件:
// size_test.go
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)
}
}
}
PS:文件名须以"_test.go"结尾;方法名须以"Test"打头,并且形参为 (t *testing.T)
运行 go test -cover
,得到测试结果:
$ go test -cover
PASS
coverage: 42.9% of statements
ok test_cover 0.125s
当我们运行 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"
}
每一个可执行的分支都被加入了一个赋值语句,当我们执行完之后,计数器开始工作并将覆盖结果返回给我们。
我们的测试结果的覆盖率很差,可以通过输出文件来看看原因:go test -coverprofile=coverage.out
coverprofile
参数会自动设置cover
参数,运行完之后,我们可以看到当前目录下存在 coverage.out 文件:
# coverage.out
mode: set
test_cover\size.go:3.25,4.12 1 1
test_cover\size.go:16.5,16.22 1 0
test_cover\size.go:5.16,6.26 1 1
test_cover\size.go:7.17,8.22 1 0
test_cover\size.go:9.17,10.23 1 1
test_cover\size.go:11.18,12.21 1 0
test_cover\size.go:13.19,14.22 1 0
我们可以分析该文件,得到每个function的覆盖率,使用命令:
$ go tool cover -func=coverage.out
test_cover\size.go:3: Size 42.9%
total: (statements) 42.9%
由于这个例子中只有一个函数,所以结果只有一个。我们可以用一个更清晰的HTML页面来显示结果:
go tool cover -html=coverage.out
此时会自动打开一个HTML页面,
在此处可以非常清晰地看到哪些分支被执行了。
Heap maps
这种代码级别的覆盖测试工具还有其他作用。比如它不仅仅可以告诉你一条语句是否被执行了,还可以告诉你它执行了多少次。
go test
接受 -covermode
参数,一共有三种设置:
- set(默认):每条语句是否被执行了?
- count:每条语句被执行了多少次?
- atomic:和count相似,但是能够在并行程序中精确计数(使用了 sync/atomic 包)。
可以按照上述方式重新进行测试,可以看到在html界面中不同覆盖率的语句用不同的颜色表示出来了。比如例子:
可以将鼠标移动到每一条语句上查看其执行次数。
计数的结果大概如下所示,左侧是执行次数:
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 }
Basic blocks
如果观察上个例子可以发现,覆盖计数无法区分一条语句中的不同部分。
f() && g()
比如这样的情况,但是你必须认识到覆盖测试是一种不精确的方法。
顺带一提的是,gcov
也存在这个问题......
总结
在源文件目录下编写xxx_test.go
文件,在其中编写TestXxx
函数,然后执行覆盖测试。
相关函数汇总:
go test -cover # 执行测试
go test -coverprofile=coverage.out --covermode=count # 执行count模式的模糊测试并保存测试结果
go tool cover -func=coverage.out # 分析每个函数执行情况
go tool cover -html=coverage.out # 显示可视化结果