乐趣区

关于golang:Go的测试覆盖率

测试覆盖率是一个术语,用于统计通过运行程序包的测试多少代码失去执行。如果执行测试套件导致 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。

退出移动版