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  					   # 显示可视化结果
posted @ 2018-06-28 22:01  四度  阅读(3664)  评论(0编辑  收藏  举报