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 包。但在某些情况下,将覆盖率插桩扩展到其他包很有用;这可以通过将 “-coverpkg” 传递给 “go build -cover” 来实现。

对于我们的示例程序,“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 构建的二进制文件,您可以通过在写入 GOCOVERDIR 目录的文件的基础上运行 go tool covdata textfmt 来事后生成文本格式的配置文件。

完成此步骤后,您可以使用 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 年第一季度结果
上一篇文章:你所有的可比较类型
博客索引