覆盖率简介
测量代码覆盖率的工具在测试套件运行时观察你的代码,并跟踪哪些行被运行,哪些没有。这种测量被称为行覆盖率,其计算方法是将运行的总行数除以代码的总行数。代码覆盖率工具还可以告诉你在控制语句中是否覆盖率了所有的路径,这种测量称为分支覆盖率。
Coverage.py是测量代码覆盖率的首选 Python 覆盖工具。pytest-cov是一个流行的 pytest 插件,经常与 coverage.py 结合使用。
在pytest-cov中使用coverage.py
$ pip install coverage
$ pip install pytest-cov
要用coverage.py运行测试,你需要添加--cov标志,并提供你要测量的代码的路径,或者你要测试的安装包。cards项目是已安装的包,所以我们用--cov=cards来测试它。
$ pytest --cov=Cards ch7
============================= test session starts =============================
platform win32 -- Python 3.9.13, pytest-7.1.2, pluggy-1.0.0
rootdir: D:\code\pytest_quick\ch7, configfile: pytest.ini
plugins: allure-pytest-2.12.0, Faker-4.18.0, tep-0.8.2, anyio-3.5.0, cov-4.0.0
collected 27 items
ch7\test_add.py ..... [ 18%]
ch7\test_config.py . [ 22%]
ch7\test_count.py ... [ 33%]
ch7\test_delete.py ... [ 44%]
ch7\test_finish.py .... [ 59%]
ch7\test_list.py .. [ 66%]
ch7\test_start.py .... [ 81%]
ch7\test_update.py .... [ 96%]
ch7\test_version.py . [100%]D:\ProgramData\Anaconda3\lib\site-packages\coverage\inorout.py:507: CoverageWarning: Module Cards was never imported. (module-not-imported)
self.warn(f"Module {pkg} was never imported.", slug="module-not-imported")
D:\ProgramData\Anaconda3\lib\site-packages\coverage\control.py:838: CoverageWarning: No data was collected. (no-data-collected)
self._warn("No data was collected.", slug="no-data-collected")
D:\ProgramData\Anaconda3\lib\site-packages\pytest_cov\plugin.py:297: CovReportWarning: Failed to generate report: No data to report.
self.cov_controller.finish()
WARNING: Failed to generate report: No data to report.
---------- coverage: platform win32, python 3.9.13-final-0 -----------
============================= 27 passed in 0.66s ==============================
# 等效的调用方法
$ coverage run --source=cards -m pytest ch7
.coveragerc
[paths]
source =
cards_proj/src/cards
*/site-packages/cards
要在终端报告中添加遗漏的行,我们可以重新运行测试并添加 --cov-report=term-missing 标志,或者coverage report --show-missing。
$ pytest --cov=cards --cov-report=term-missing ch7
============================= test session starts =============================
platform win32 -- Python 3.9.13, pytest-7.1.2, pluggy-1.0.0
rootdir: D:\code\pytest_quick\ch7, configfile: pytest.ini
plugins: allure-pytest-2.12.0, Faker-4.18.0, tep-0.8.2, anyio-3.5.0, cov-4.0.0
collected 27 items
ch7\test_add.py ..... [ 18%]
ch7\test_config.py . [ 22%]
ch7\test_count.py ... [ 33%]
ch7\test_delete.py ... [ 44%]
ch7\test_finish.py .... [ 59%]
ch7\test_list.py .. [ 66%]
ch7\test_start.py .... [ 81%]
ch7\test_update.py .... [ 96%]
ch7\test_version.py . [100%]
---------- coverage: platform win32, python 3.9.13-final-0 -----------
Name Stmts Miss Cover Missing
----------------------------------------------------------------
cards_proj\src\cards\__init__.py 3 0 100%
cards_proj\src\cards\api.py 70 3 96% 72, 78, 82
cards_proj\src\cards\cli.py 86 53 38% 20, 28-30, 36-40, 51-63, 73-80, 86-90, 96-100, 106-107, 113-114, 122-123, 127-132, 137-140
cards_proj\src\cards\db.py 23 0 100%
----------------------------------------------------------------
TOTAL 182 56 69%
============================= 27 passed in 0.75s ==============================
$ coverage report --show-missing
Name Stmts Miss Cover Missing
----------------------------------------------------------------
cards_proj\src\cards\__init__.py 3 0 100%
cards_proj\src\cards\api.py 70 3 96% 72, 78, 82
cards_proj\src\cards\cli.py 86 53 38% 20, 28-30, 36-40, 51-63, 73-80, 86-90, 96-100, 106-107, 113-114, 122-123, 127-132, 137-140
cards_proj\src\cards\db.py 23 0 100%
----------------------------------------------------------------
TOTAL 182 56 69%
参考资料
- 本文相关海量书籍下载
生成HTML报告
$ cd /path/to/code
$ pytest --cov=cards --cov-report=html ch7
# 或
$ pytest --cov=cards ch7
$ coverage html
这两条命令都要求coverage.py生成HTML报告。该报告被称为htmlcov/,在你运行该命令的目录中。
用浏览器打开htmlcov/index.html,你会看到。
点击api.py文件会显示该文件的报告,如图所示。
报告的顶部显示了所覆盖的行的百分比(96%),语句的总数(72),以及有多少语句被运行(69),遗漏(3)和排除(0)。向下滚动,你可以看到被遗漏的高亮行。
ist_cards()函数有几个可选参数--owner和state,允许对列表进行过滤。这些行没有被我们的测试套件所测试。
将代码排除在覆盖范围之外
if __name__ == '__main__': # pragma: no cover
exit(main())
为了达到100%的覆盖率而添加测试的问题是,这样做会掩盖这些行没有被使用的事实,因此应用程序不需要。它还会增加不必要的测试代码和编码时间。
在测试中运行覆盖率
除了使用覆盖率来确定我们的测试套件是否击中了代码外。让我们把我们的测试目录添加到覆盖率报告中。
$ pytest --cov=cards --cov=ch7 ch7
============================= test session starts =============================
platform win32 -- Python 3.9.13, pytest-7.1.2, pluggy-1.0.0
rootdir: D:\code\pytest_quick\ch7, configfile: pytest.ini
plugins: allure-pytest-2.12.0, Faker-4.18.0, tep-0.8.2, anyio-3.5.0, cov-4.0.0
collected 27 items
ch7\test_add.py ..... [ 18%]
ch7\test_config.py . [ 22%]
ch7\test_count.py ... [ 33%]
ch7\test_delete.py ... [ 44%]
ch7\test_finish.py .... [ 59%]
ch7\test_list.py .. [ 66%]
ch7\test_start.py .... [ 81%]
ch7\test_update.py .... [ 96%]
ch7\test_version.py . [100%]
---------- coverage: platform win32, python 3.9.13-final-0 -----------
Name Stmts Miss Cover
------------------------------------------------------
cards_proj\src\cards\__init__.py 3 0 100%
cards_proj\src\cards\api.py 70 3 96%
cards_proj\src\cards\cli.py 86 53 38%
cards_proj\src\cards\db.py 23 0 100%
ch7\conftest.py 22 0 100%
ch7\test_add.py 31 0 100%
ch7\test_config.py 2 0 100%
ch7\test_count.py 9 0 100%
ch7\test_delete.py 28 0 100%
ch7\test_finish.py 13 0 100%
ch7\test_list.py 11 0 100%
ch7\test_start.py 13 0 100%
ch7\test_update.py 21 0 100%
ch7\test_version.py 5 0 100%
------------------------------------------------------
TOTAL 337 56 83%
============================= 27 passed in 0.71s ==============================
--cov=cards命令告诉coverage观察card包。--cov=ch7命令告诉覆盖率观察ch7目录,也就是我们的测试所在的地方。
在所有的编程中,特别是在编码测试中,一个常见的错误是用复制/粘贴/修改的方式添加新的测试函数。对于新的测试函数,我们可能会复制现有的函数,将其粘贴为新的函数,并修改代码以满足新的测试案例。如果我们忘记改变函数的名称,那么两个函数就会有相同的名称,而且只有文件中的最后一个函数会被运行。重复命名的测试的问题很容易被抓住,包括你的测试代码在覆盖率的来源。
类似的问题也会发生在大型测试模块中,当我们只是忘记了所有的函数名称,而不小心将第二个测试函数的名称与前一个函数的名称相同。
第三个问题更微妙。覆盖率有能力结合几个测试环节的报告。这是必要的,例如,在持续集成的不同硬件上进行测试。一些测试可能是特定于某些硬件的,而在其他硬件上被跳过。综合报告,如果我们包括测试,将帮助我们确保我们所有的测试最终至少在一些硬件上运行。它还有助于找到未使用的固定装置,或固定装置中的死代码。
在目录上运行覆盖率
在 ch9/some_code 目录中,我们有几个源代码模块和一个测试模块。
ch9/some_code
├──bar_module.py
├──foo_module.py
└── test_some_code.py
执行
$ pytest --cov=ch9/some_code ch9/some_code/test_some_code.py
============================= test session starts =============================
platform win32 -- Python 3.9.13, pytest-7.1.2, pluggy-1.0.0
rootdir: D:\code\pytest_quick, configfile: pytest.ini
plugins: allure-pytest-2.12.0, Faker-4.18.0, tep-0.8.2, anyio-3.5.0, cov-4.0.0
collected 2 items
ch9\some_code\test_some_code.py .. [100%]
---------- coverage: platform win32, python 3.9.13-final-0 -----------
Name Stmts Miss Cover
-----------------------------------------------------
ch9\some_code\bar_module.py 4 1 75%
ch9\some_code\foo_module.py 2 0 100%
ch9\some_code\test_some_code.py 6 0 100%
-----------------------------------------------------
TOTAL 12 1 92%
============================== 2 passed in 0.14s ==============================
# 或者
$ pytest --cov=some_code some_code/test_some_code.py
# 或者
$ pytest --cov=some_code some_code
统计单个文件覆盖率
ch9/single_file.py
def foo():
return "foo"
def bar():
return "bar"
def baz():
return "baz"
def main():
print(foo(), baz())
if __name__ == "__main__": # pragma: no cover
main()
# test code, requires pytest
def test_foo():
assert foo() == "foo"
def test_baz():
assert baz() == "baz"
def test_main(capsys):
main()
captured = capsys.readouterr()
assert captured.out == "foo baz\n"
下面是它的运行情况。
$ pytest --cov=single_file single_file.py
============================= test session starts =============================
platform win32 -- Python 3.9.13, pytest-7.1.2, pluggy-1.0.0
rootdir: D:\code\pytest_quick, configfile: pytest.ini
plugins: allure-pytest-2.12.0, Faker-4.18.0, tep-0.8.2, anyio-3.5.0, cov-4.0.0
collected 3 items
single_file.py ... [100%]
---------- coverage: platform win32, python 3.9.13-final-0 -----------
Name Stmts Miss Cover
------------------------------------
single_file.py 16 1 94%
------------------------------------
TOTAL 16 1 94%
============================== 3 passed in 0.16s ==============================