增加测试覆盖率

Python 开发遵循一项惯例,即语言和 stdlib 的所有语义更改和新增内容都附带适当的单元测试。遗憾的是,Python 在这项惯例生效前就已经存在很长时间了。这导致 stdlib 中存在大量未经测试的部分,而这并不是一个理想的情况。

熟悉 Python 代码并提供帮助的一种简单易行的方法是帮助提高 Python stdlib 的测试覆盖率。理想情况下,我们希望达到 100% 的覆盖率,但任何提高都是好的。不过,请注意,获得 100% 的覆盖率并不总是可能的。可能存在仅对您无法执行的特定于平台的代码、输出中的错误等。您可以根据自己的判断来决定应该覆盖哪些内容,但不应该覆盖哪些内容,但通常遵循保守原则并假设应该覆盖某些内容是一个好规则。

可以选择要提高其测试覆盖率的模块可以通过多种方式进行。您可以简单地自己运行整个测试套件,同时打开覆盖率,看看哪些模块需要帮助。这有一个缺点,即在覆盖率测量下运行整个测试套件需要一些时间才能完成,但您将准确地了解哪些模块需要做最多的工作。

另一种方法是遵循以下示例,看看您最喜欢的模块有什么覆盖率。不过,这是“盲人摸象”,因此可能需要一些时间才能找到需要覆盖率帮助的模块。

不过,请务必确保对于您决定处理的任何模块,您只运行该模块的覆盖率。这将确保您知道该模块的显式覆盖率从其自己的测试集中有多好,而不是从碰巧使用该模块的其他代码的隐式测试中获得的。

常见的陷阱

请注意,在开始记录覆盖率数据之前已经导入的模块的覆盖率报告将是错误的。通常,您可以通过覆盖率报告来判断模块是否属于此类别,该报告指出显然会在导入时执行的全局语句未执行,而局部语句已覆盖。在这些情况下,您可以忽略全局语句覆盖率,而只关注局部语句覆盖率。

在编写新测试以提高覆盖率时,请注意已为模块提供的测试样式(例如,白盒、黑盒等)。由于某些模块主要由单个核心开发人员维护,因此他们可能对使用哪种类型的测试(例如,白盒)有特定的偏好,并希望不使用其他类型的测试(例如,黑盒)。如有疑问,请坚持使用白盒测试以正确执行代码。

测量覆盖率

需要注意的是,在 Python 自己的 stdlib 上运行覆盖率的一个怪癖是,某些模块在解释器启动时作为其一部分被导入。Python 本身需要的那些模块不会被覆盖率工具视为已执行,因此看起来它们的覆盖率很差(例如,stat 模块)。在这些情况下,该模块似乎没有任何全局语句覆盖率,但将具有适当的局部语句覆盖率(例如,不会跟踪函数定义,但会跟踪函数体)。在这种情况下计算模块的覆盖率只需要手动查看未执行的局部语句即可。

使用 coverage.py

最流行的第三方覆盖率工具之一是 coverage.py,它提供了非常好的 HTML 输出以及高级功能,例如 分支覆盖率。如果您希望只使用 stdlib 提供的工具,则可以 使用 test.regrtest

安装 coverage

默认情况下,pip 不会安装到您刚刚构建的 Python 开发版本中,并且此构建版本的 Python 不会看到安装到您的 Python 默认版本中的包。一种选择是使用虚拟环境来安装 coverage。

运行

./python -m venv ../cpython-venv
source ../cpython-venv/bin/activate
pip install coverage

大多数 macOS 系统上运行

./python.exe -m venv ../cpython-venv
source ../cpython-venv/bin/activate
pip install coverage

运行

python.bat -m venv ..\\cpython-venv
..\\cpython-venv\\Scripts\\activate.bat
pip install coverage

只要您的 venv 已激活,您现在就可以在这些说明的其余部分中使用 python 而无需 ./。有关 venv 的更多信息,请参阅 虚拟环境 文档。

如果由于某种原因这对您不起作用,您应该尝试使用 coverage.py 的开发版本,看看它是否已根据需要更新。为此,您应该克隆/签出 coverage.py 的开发版本

git clone https://github.com/nedbat/coveragepy

您需要使用安装的完整路径。

另一种选择是使用已安装的 coverage.py 副本(如果您已经拥有)。为此,您需要再次使用该安装的完整路径。

基本用法

以下命令将告诉您 coverage 的副本是否有效(用 COVERAGEDIR 替换克隆副本所在的目录,例如 ../coveragepy

./python COVERAGEDIR

Coverage.py 将打印一些帮助文本,验证一切是否正常。如果您使用的是已安装的副本,则可以执行以下操作(请注意,必须使用 Python 的内置副本进行安装,例如 venv)

./python -m coverage

有关如何使用 coverage.py 的其余示例将假定您使用的是克隆副本,但您可以替换上述内容,所有说明仍然有效。

若要在 coverage.py 下运行测试套件,请执行以下操作

./python COVERAGEDIR run --pylib Lib/test/regrtest.py

若要仅运行单个测试,请在 --source 标志中指定要测试的模块/包(以便将覆盖范围报告仅限于您感兴趣的模块/包),然后将您希望运行的测试的名称附加到命令

./python COVERAGEDIR run --pylib --source=abc Lib/test/regrtest.py test_abc

若要查看覆盖运行的结果,您可以使用以下命令查看基于文本的报告

./python COVERAGEDIR report

您可以使用 --show-missing 标志获取未执行的行列表

./python COVERAGEDIR report --show-missing

但是 coverage.py 的一个优点是其基于 HTML 的报告,它可以让您直观地看到哪些代码行未经过测试

./python COVERAGEDIR html -i --include=`pwd`/Lib/* --omit="Lib/test/*,Lib/*/tests/*"

这将在名为 htmlcov 的目录中生成一个 HTML 报告,该报告会忽略可能出现的任何错误,并忽略测试覆盖范围不重要的模块(例如测试、临时文件等)。然后,您可以在 Web 浏览器中打开 htmlcov/index.html 文件,以查看覆盖结果以及明显显示哪些代码行已执行或未执行的页面。

分支覆盖范围

对于真正大胆的人,您可以使用 coverage.py 的另一个强大功能:分支覆盖范围。测试代码中的每条可能的分支路径,虽然是一个值得努力实现的伟大目标,但却是为整个 stdlib 获得 100% 行覆盖范围的次要目标(目前)。

如果您决定尝试提高分支覆盖范围,只需将 --branch 标志添加到您的覆盖运行中

./python COVERAGEDIR run --pylib --branch <arguments to run test(s)>

这将导致报告不仅说明了哪些行未覆盖,还说明了哪些分支路径未执行。

使用 test.regrtest

如果您希望仅依靠 stdlib 生成覆盖范围数据,可以通过将适当的标志传递给 test 来实现(以及您想要的任何其他标志)

./python -m test --coverage -D `pwd`/coverage_data <test arguments>

请注意 -D 的参数;如果您未指定覆盖数据最终位置的绝对路径,它将转到您意想不到的地方。

注意

如果您在整个测试套件上运行覆盖范围,请务必添加 -x test_importlib test_runpy test_trace 以排除这些测试,因为它们会在覆盖范围期间触发异常;请参阅 python/cpython#54750python/cpython#55200

测试完成后,您会发现指定的目录包含每个已执行模块的文件以及每行执行了多少次。

提交问题

提高覆盖范围后,您需要在 问题跟踪器 上创建一个问题并提交 请求合并

使用 gcov 和 lcov 测量 C 代码的覆盖范围

还可以测量 Python 的 C 代码的功能、行和分支覆盖范围。现在仅支持带有 gcov 的 GCC。为了使用 gcov 创建 Python 的工具化构建,请运行

make coverage
.\make coverage

然后运行一些代码并使用 gcov 命令收集覆盖率数据。为了创建 HTML 报告,你可以安装 lcov。该命令

make coverage-lcov
.\make coverage-lcov

汇编覆盖率数据,移除第三方和系统库,最后创建报告。你可以跳过这两个步骤,直接运行

make coverage-report
.\make coverage-report

如果你想为 Python 的 stdlib 测试生成覆盖率报告。在现代计算机上大约需要 20 到 30 分钟。

注意

多个测试作业可能无法正常工作。C 覆盖率报告仅在单个测试进程中经过测试。