单元测试的最终统计标准就是单测覆盖率,统计单测总体覆盖了多少行代码。一般来说,我们只需要关注增量代码的覆盖率,而非全量代码。增量代码就是本次迭代改动的代码,比如本次迭代改动了100行代码,我们保证单测能覆盖到这 100 行代码就行。

关于单测,你应该会对下面的文章感兴趣:

GoMonkey Patching in Go,通过 mock 的方式来辅助单测,能够mock一切的函数和方法,使用简单,功能强大

go test coverage 单测覆盖率 也假装了解一下 go 单测覆盖率的知识(尴尬,链接还是这篇博客)。

一般测试开发都会集成一套单测系统,当我们发布上线的时候,自动检测代码的增量覆盖率。假如覆盖率不达标,不好意思,你的代码被判定有风险,禁止发布。增量覆盖率应该设置多少的阈值比较合适呢,反正标准是越来越高了。

不过,你说做完了这些就能保证代码没有问题吗?很多人只是为了单测覆盖率而单测,只追求单测覆盖率那个冰冷的数字,丢失了单测原本的意义。不过,作为一个任劳任怨的小蚂蚁,还是要好好了解一下单测覆盖率的,虽然了解也对你用处也不大。

命令行查看单测覆盖

在命令行覆盖率的统计指令,会生成一个 coverage 文件,基于这个 coverage 文件,我们结合 go tool 工具,就可以图形话展示代码覆盖率情况。

关于下面的命令的功能,简单的说就是执行目录下的所有单测用例,然后将数据的统计结果汇总到 coverage.out 这个文件下,然后用另外一个命令来解析这个文件。

万变不离其宗,任各个平台完的再花里胡哨,也都是这种模式。关于命令行的属性,记不记得住其实也没有那么重要,看看这个命令能干啥就够了…

go test -coverprofile=coverage.out -covermode=atomic ./...

指令中 ./… 是 go 的标准用法,会递归执行目录下的所有单测文件,最终,生成的 coverage 文件会保存在属性 -coverprofile 指定的文件中。而我们通过下面的指令就可以图形化展示覆盖率

go tool cover -html=coverage.out

通过这种方式,我们选取一个简单的单测,执行上面的两个指令,会在浏览器中展示下图的示例,其中,绿色的代码表示被单测覆盖,红色表示未被覆盖,灰色表示没有做覆盖率埋点。关于覆盖率埋点,下面单测文件的覆盖率是 33.3%,也就是有 1/3 的代码被覆盖了。

表面上看的话,下面有效的代码总共有3行,其中1行被覆盖到了,所以,覆盖率是 33.3%

单测 not prepare for test_单元测试

新增代码覆盖率没有被统计

在统计覆盖率的过程中,通过断点调试,我肯定单测一定走到了新增代码的逻辑里,但是新增代码的覆盖率却没有被统计到,我觉得很奇怪。在 goland 编译器中加载 coverage.out 文件分析,新增代码覆盖率也没有被统计到。下面是代码的结构目录:

├── service
│			└── point.go
├── controller
│			├── endpoint.go
│			└── endpoint_test.go

在 endpoint_test 中的单测只能覆盖到 controller 包中的代码,尽管 endpoint 中的代码引用到了 service 包中的 point 文件,但是 point 中的代码覆盖率并不能被统计到。这样的结果其实也可以理解,单测的对象应该是包中的局部方法,不统计依赖包中的方法也是合理的做法。

有没有属性可以解决这个问题呢?当然有,但是需要修改一下上面的认知:go test 在覆盖率计算中只考虑带有测试文件的软件包。 但可以使用 -coverpkg 将所有软件包添加到覆盖率计算中

go test ./... -coverpkg=./...

covermode 属性

我有好奇 covermode 这个属性的意图是什么,在The cover story中有这个属性的介绍,子标题是 heat map,热力图。当我们不仅仅想知道代码行是否被执行,还想知道代码被执行了多少次,就要靠这个属性。

如果不指定这个属性,默认值是 set,表示只统计语句是否被执行。另外的两个属性count、atomic 被用来统计语句被执行的次数信息。这两个属性的差异,文章中也有介绍。atomic 属于原子计数,统计结果会更加精确。

- count: how many times did each statement run?
- atomic: like count, but counts precisely in parallel programs

排除覆盖率统计

代码中可能会存在一些自动生成的代码,这些代码往往还会加一行醒目的注释:“DO NOT EDIT”,这部分代码往往不需要执行单元测试,还因为这些的存在,代码覆盖率还会被拉得特别低。

这对这种业务上无须进行代码覆盖率统计的文件,我们需要在执行覆盖率统计之前,明确的将其排除。在 go test 指令中有没有指令可以指定呢?

有,而且大家已经见过它了,还是 -coverpkg。这个属性值需要合理的配合 … 来使用,它支持具体的单测包、或者就是具体的单测文件,当然也支持排除,命令符号是 !,!表示排除,多个文件或者包之间使用逗号进行分隔。比方说,我们统计当前目录下的 controller 包,排除当前目录下的 service 包

在goland中查看覆盖率

在 goland 中直接运行单测,选择「Run with Coverage」会执行覆盖率统计,这个功能就是单测包中绿色的执行标。它包含包几倍和方法级别的。

下面是 goland 中覆盖率的执行效果,绿色表示代码被覆盖,红色表示代码未被覆盖,项目栏中显示了代码的覆盖率。

单测 not prepare for test_代码覆盖率_02

还可以直接使用命令行生成的 coverage.out 文件,goland 也支持直接解析它,最终的效果和在 goland 中直接执行 「Run with Coverage」效果一模一样。执行「Run」-> 「Show Coverage Data…」加载覆盖率报告就可以了。

单测 not prepare for test_命令行_03

总结

测试能证明缺陷存在,而无法证明没有缺陷。关于如何写单测,欢迎大家浏览 Go 单元测试 和 Testify Mock 单元测试