Go 博客

Go 集成测试的代码覆盖率

Than McIntosh
2023 年 3 月 8 日

代码覆盖率工具帮助开发者确定当执行给定的测试套件时,源代码库的多少部分被执行(覆盖)。

Go 在一段时间以来一直提供支持(在 Go 1.2 版本中引入),使用“go test”命令的 “-cover” 标志在包级别测量代码覆盖率。

这个工具在大多数情况下运行良好,但对于大型 Go 应用来说存在一些不足。对于这类应用,开发者除了包级别的单元测试外,通常会编写“集成”测试来验证整个程序的行为。

这种类型的测试通常涉及构建一个完整的应用程序二进制文件,然后在一组具有代表性的输入上(或者如果是服务器,则在生产负载下)运行该二进制文件,以确保所有组件包协同工作正常,而不是孤立地测试单个包。

由于集成测试二进制文件是使用“go build”构建的,而不是“go test”,因此 Go 的工具到目前为止还没有提供任何简单的方法来收集这些测试的覆盖率配置文件。

借助 Go 1.20,您现在可以使用“go build -cover”构建带有覆盖率检测的程序,然后将这些检测过的二进制文件用于集成测试,从而扩展覆盖率测试的范围。

在这篇博文中,我们将举例说明这些新功能的工作原理,并概述从集成测试中收集覆盖率配置文件的一些用例和工作流程。

示例

让我们以一个非常小的示例程序为例,为其编写一个简单的集成测试,然后从集成测试中收集覆盖率配置文件。

在本练习中,我们将使用来自 gitlab.com/golang-commonmark/mdtool 的“mdtool” Markdown 处理工具。这是一个演示程序,旨在展示客户端如何使用 gitlab.com/golang-commonmark/markdown 包(一个 Markdown 到 HTML 的转换库)。

mdtool 设置

首先,我们下载一份“mdtool”本身(我们选择一个特定版本只是为了使这些步骤可重现)

$ git clone https://gitlab.com/golang-commonmark/mdtool.git
...
$ cd mdtool
$ git tag example e210a4502a825ef7205691395804eefce536a02f
$ git checkout example
...
$

一个简单的集成测试

现在我们将为“mdtool”编写一个简单的集成测试;我们的测试将构建“mdtool”二进制文件,然后在测试数据目录中的一组输入 Markdown 文件上运行它。这个非常简单的脚本在测试数据目录中的每个文件上运行“mdtool”二进制文件,检查以确保它能产生输出并且不会崩溃。

$ cat integration_test.sh
#!/bin/sh
BUILDARGS="$*"
#
# Terminate the test if any command below does not complete successfully.
#
set -e
#
# Download some test inputs (the 'website' repo contains various *.md files).
#
if [ ! -d testdata ]; then
  git clone https://go.googlesource.com/website testdata
  git -C testdata tag example 8bb4a56901ae3b427039d490207a99b48245de2c
  git -C testdata checkout example
fi
#
# Build mdtool binary for testing purposes.
#
rm -f mdtool.exe
go build $BUILDARGS -o mdtool.exe .
#
# Run the tool on a set of input files from 'testdata'.
#
FILES=$(find testdata -name "*.md" -print)
N=$(echo $FILES | wc -w)
for F in $FILES
do
  ./mdtool.exe +x +a $F > /dev/null
done
echo "finished processing $N files, no crashes"
$

以下是我们测试的一个运行示例

$ /bin/sh integration_test.sh
...
finished processing 380 files, no crashes
$

成功:我们已经验证了“mdtool”二进制文件成功处理了一组输入文件……但是我们实际执行了该工具多少源代码呢?在下一节中,我们将收集覆盖率配置文件来找出答案。

使用集成测试收集覆盖率数据

让我们编写另一个包装脚本,它调用之前的脚本,但构建用于覆盖率分析的工具,然后对生成的配置文件进行后处理

$ cat wrap_test_for_coverage.sh
#!/bin/sh
set -e
PKGARGS="$*"
#
# Setup
#
rm -rf covdatafiles
mkdir covdatafiles
#
# Pass in "-cover" to the script to build for coverage, then
# run with GOCOVERDIR set.
#
GOCOVERDIR=covdatafiles \
  /bin/sh integration_test.sh -cover $PKGARGS
#
# Post-process the resulting profiles.
#
go tool covdata percent -i=covdatafiles
$

关于上面这个包装器需要注意的几个关键点

  • 它在运行 integration_test.sh 时传入了 “-cover” 标志,这使我们得到了一个经过覆盖率检测的 “mdtool.exe” 二进制文件
  • 它设置了 GOCOVERDIR 环境变量,指定了覆盖率数据文件将写入的目录
  • 测试完成后,它运行 “go tool covdata percent” 生成关于语句覆盖率百分比的报告

以下是我们运行这个新包装脚本时的输出

$ /bin/sh wrap_test_for_coverage.sh
...
    gitlab.com/golang-commonmark/mdtool coverage: 48.1% of statements
$
# Note: covdatafiles now contains 381 files.

瞧!我们现在对集成测试在执行“mdtool”应用程序源代码方面做得如何有了大概了解。

如果我们对测试工具进行更改以增强它,然后再次进行覆盖率收集运行,我们将在覆盖率报告中看到这些更改的体现。例如,假设我们通过在 integration_test.sh 中添加以下两行额外代码来改进测试:

./mdtool.exe +ty testdata/README.md  > /dev/null
./mdtool.exe +ta < testdata/README.md  > /dev/null

再次运行覆盖率测试包装器

$ /bin/sh wrap_test_for_coverage.sh
finished processing 380 files, no crashes
    gitlab.com/golang-commonmark/mdtool coverage: 54.6% of statements
$

我们可以看到更改的效果:语句覆盖率从 48% 增加到了 54%。

选择需要覆盖的包

默认情况下,“go build -cover”只会检测构建的 Go 模块中的包,在此例中是 gitlab.com/golang-commonmark/mdtool 包。但在某些情况下,将覆盖率检测扩展到其他包是有用的;这可以通过向“go build -cover”传递“-coverpkg”来实现。

对于我们的示例程序,“mdtool”实际上很大程度上只是一个围绕 gitlab.com/golang-commonmark/markdown 包的包装器,因此将 markdown 包含在检测的包集合中是很有意义的。

这是“mdtool”的 go.mod 文件

$ head go.mod
module gitlab.com/golang-commonmark/mdtool

go 1.17

require (
    github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8
    gitlab.com/golang-commonmark/markdown v0.0.0-20211110145824-bf3e522c626a
)

我们可以使用“-coverpkg”标志来控制哪些包被选中包含在覆盖率分析中,以包含上述某个依赖项。这是一个例子:

$ /bin/sh wrap_test_for_coverage.sh -coverpkg=gitlab.com/golang-commonmark/markdown,gitlab.com/golang-commonmark/mdtool
...
    gitlab.com/golang-commonmark/markdown   coverage: 70.6% of statements
    gitlab.com/golang-commonmark/mdtool coverage: 54.6% of statements
$

处理覆盖率数据文件

当覆盖率集成测试完成并写出一组原始数据文件(在我们的例子中,是 covdatafiles 目录的内容)后,我们可以通过多种方式对这些文件进行后处理。

将配置文件转换为 ‘-coverprofile’ 文本格式

在使用单元测试时,您可以运行 go test -coverprofile=abc.txt 来为给定的覆盖率测试运行写入文本格式的覆盖率配置文件。

对于使用 go build -cover 构建的二进制文件,您可以在事后运行 go tool covdata textfmt 处理写入 GOCOVERDIR 目录的文件来生成文本格式的配置文件。

完成此步骤后,您可以使用 go tool cover -func=<file>go tool cover -html=<file> 来解释/可视化数据,就像您使用 go test -coverprofile 一样。

示例

$ /bin/sh wrap_test_for_coverage.sh
...
$ go tool covdata textfmt -i=covdatafiles -o=cov.txt
$ go tool cover -func=cov.txt
gitlab.com/golang-commonmark/mdtool/main.go:40:     readFromStdin   100.0%
gitlab.com/golang-commonmark/mdtool/main.go:44:     readFromFile    80.0%
gitlab.com/golang-commonmark/mdtool/main.go:54:     readFromWeb 0.0%
gitlab.com/golang-commonmark/mdtool/main.go:64:     readInput   80.0%
gitlab.com/golang-commonmark/mdtool/main.go:74:     extractText 100.0%
gitlab.com/golang-commonmark/mdtool/main.go:88:     writePreamble   100.0%
gitlab.com/golang-commonmark/mdtool/main.go:111:    writePostamble  100.0%
gitlab.com/golang-commonmark/mdtool/main.go:118:    handler     0.0%
gitlab.com/golang-commonmark/mdtool/main.go:139:    main        51.6%
total:                          (statements)    54.6%
$

使用 ‘go tool covdata merge’ 合并原始配置文件

每次执行由 “-cover” 构建的应用程序都会将一个或多个数据文件写入 GOCOVERDIR 环境变量指定的目录。如果一个集成测试执行 N 次程序运行,您的输出目录中将有 O(N) 个文件。数据文件中通常有很多重复内容,因此为了压缩数据和/或合并来自不同集成测试运行的数据集,您可以使用 go tool covdata merge 命令来合并配置文件。例如:

$ /bin/sh wrap_test_for_coverage.sh
finished processing 380 files, no crashes
    gitlab.com/golang-commonmark/mdtool coverage: 54.6% of statements
$ ls covdatafiles
covcounters.13326b42c2a107249da22f6e0d35b638.772307.1677775306041466651
covcounters.13326b42c2a107249da22f6e0d35b638.772314.1677775306053066987
...
covcounters.13326b42c2a107249da22f6e0d35b638.774973.1677775310032569308
covmeta.13326b42c2a107249da22f6e0d35b638
$ ls covdatafiles | wc
    381     381   27401
$ rm -rf merged ; mkdir merged ; go tool covdata merge -i=covdatafiles -o=merged
$ ls merged
covcounters.13326b42c2a107249da22f6e0d35b638.0.1677775331350024014
covmeta.13326b42c2a107249da22f6e0d35b638
$

go tool covdata merge 操作也接受 -pkg 标志,如果需要,可以使用该标志选择特定的包或一组包。

这种合并功能也适用于合并不同类型测试运行的结果,包括由其他测试工具生成的运行结果。

总结

这就是全部内容:随着 1.20 版本的发布,Go 的覆盖率工具不再局限于包测试,而是支持从大型集成测试中收集配置文件。我们希望您能充分利用这些新功能,帮助理解您更大、更复杂的测试运行得如何,以及它们正在执行您的源代码的哪些部分。

请尝试这些新功能,如果遇到问题,一如既往地在我们的 GitHub 问题跟踪器 上提交问题。谢谢。

下一篇文章:Go 开发者调查 2023 年第一季度结果
上一篇文章:所有可比较的类型
博客索引