- 代码覆盖率 衡量测试期间运行的代码量。
- 分支覆盖率比线路覆盖率更严格,也更有用。
- 在我们的测试中,无断言 的套件仍达到 100% 的覆盖率。
- 使用覆盖率来查找未经测试的代码,而不是作为质量证明。
一个团队自豪地报告了 95% 的代码覆盖率,然后在“覆盖”的行中发布了一个错误。这并不矛盾。覆盖率回答了一个狭隘的问题:测试是否执行了这段代码?他们是否检查结果完全是另一个问题。
我们使用带有coverage.py 7.17 和pytest 9.2 的Python。下面的每个数字和报告均来自我们自己的运行。这些想法适用于任何语言。
什么是代码覆盖率?
代码覆盖率是自动化测试运行时运行的代码的百分比。工具记录执行了哪些行,以及(可选)执行了哪些分支,然后报告遗漏的内容。
反向时最有用。某个区域的低覆盖率可以肯定地告诉您,测试没有发挥作用。高覆盖率告诉您的信息要少得多。
承保类型
| 类型 | 提问它回答 |
|---|---|
| 功能 | 每个函数至少被调用一次吗? |
| 行(语句) | 每条线至少运行一次吗? |
| 分公司 | 每个 if 都是双向的吗?是真还是假? |
| 条件 | 组合条件 (a and b) 的每个部分是否都采用这两个值? |
| MC/DC | 每个条件是否独立地改变了结果?用于安全关键软件。 |
每个级别都比其上一级更严格。大多数团队都会跟踪线路和分支覆盖范围。条件覆盖和 MC/DC 出现在航空和汽车标准中,其中工具支持是专门化的。
我们测量的代码
def shipping_fee(total, is_member, express=False):
if total < 0:
raise ValueError("total cannot be negative")
fee = 0 if total >= 1000 else 50
if is_member:
fee = max(fee - 25, 0)
if express:
fee += 100
return fee
我们的第一个测试文件有两个测试:
def test_regular_order():
assert shipping_fee(500, False) == 50
def test_member_express():
assert shipping_fee(500, True, express=True) == 125
我们在打开分支测量的情况下运行它:
coverage run --branch -m pytest test_shipping.py
coverage report -m
阅读报告
Name Stmts Miss Branch BrPart Cover Missing
---------------------------------------------------------
shipping.py 9 1 6 1 87% 3
- Stmts 9,Miss 1: 九个可执行语句,其中一个从未运行过。
- Branch 6,BrPart 1: 三个
if决策给出了六种可能的结果。一项决定只能以一种方式进行。 - 缺少 3: 第 3 行,即
raise ValueError,从未运行过。没有测试通过负总数。
该修复是对错误路径的测试。我们还添加了针对大订单的测试:
def test_free_over_1000():
assert shipping_fee(1000, False) == 0
def test_negative_total():
with pytest.raises(ValueError):
shipping_fee(-1, False)
对于这两个文件,报告显示 100%,并且没有丢失行或部分分支。错误路径是经典的间隙覆盖发现,这就是为什么使用 等价分区 等技术设计的无效输入测试与它配合得很好。
报告隐藏了什么
再看一下第一份报告。据说只有3号线缺失。但前两次测试总共使用了 500 个,因此 1000 个或以上订单的免费送货规则是从未测试过,报告也没有标记它。
原因是 fee = 0 if total >= 1000 else 50 位于一行。 coverage.py 测量行之间的分支,因此单行内的条件表达式算作一个执行的语句。如果该规则存在错误,例如 > 而不是 >=,则第一组仍会显示 87%,而没有任何提示。正如边界值分析中所述,边界本身的测试可以捕获它。
100% 覆盖,无需检查
接下来,我们故意编写了一个糟糕的测试,它调用每个路径但不声明任何内容:
def test_runs_everything_but_checks_nothing():
shipping_fee(500, True, express=True)
shipping_fee(1000, False)
shipping_fee(500, False)
try:
shipping_fee(-1, False)
except ValueError:
pass
shipping.py 9 0 6 0 100%
达到了100%的线路和分支覆盖率。该函数可以返回任何数字,并且该测试仍然会通过。这是覆盖范围的核心限制:它衡量执行,而不是验证。
提示: 在审查拉取请求时,请阅读断言,而不仅仅是覆盖率更改。在没有有意义的断言的情况下增加覆盖范围的测试只会增加错误的信心。
要衡量测试是否真正捕获错误,请使用突变测试。 Python 的 mutmut、Java 的 PIT 或 JavaScript 的 Stryker 等工具会对代码进行一些小更改,例如将 >= 翻转为 >,并检查测试是否失败。我们的无断言测试会让每个突变都存活下来。
设定什么目标
没有普遍正确的数字。实用指导:
- 不要追求 100%。 最后几个百分点通常意味着测试琐碎的代码或编写仅接触行的测试。
- 许多团队的总体目标是大约 70% 到 85%,对核心业务逻辑有更高的期望。
- 关注趋势,而不是数字。 每次发布的覆盖率下降是一个警告信号。
- 要求在拉取请求中覆盖新代码,而不是强制一次全部更新旧代码。
- 排除无关紧要的内容,例如生成的代码,具有明确的规则。
古德哈特定律适用:当一项措施成为目标时,它就不再是一个好的措施。硬目标鼓励像我们的无断言示例这样的测试。
按语言分类的工具
| 语言 | 常用工具 |
|---|---|
| 蟒蛇 | 覆盖率.py、pytest-cov |
| JavaScript / TypeScript | 伊斯坦布尔(纽约),内置于 Jest 和 Vitest 中 |
| 爪哇 | 嘉可可 |
| .NET | 被单 |
| 去 | 去测试覆盖 |
| 报告 | Codecov、SonarQube、工作服 |
CI 的覆盖范围
将覆盖率作为 CI 管道 的一部分运行,并发布报告,以便审阅者可以查看哪些新行未经测试。对于coverage.py,coverage report --fail-under=80 构建失败低于阈值。保持适度的阈值并将其与新代码的规则结合起来,这样单个大型旧文件就不会阻止每个更改。
超出覆盖范围:要问更好的问题
- 测试是否涵盖了代码中存在风险、经常更改的部分?
- 是否测试了边界和错误路径,而不仅仅是快乐路径?
- 如果这条线错误,测试会失败吗?
- 测试是否足够快速和稳定,可以在每次更改时运行?
这些属于任何测试策略。报道只能帮助回答第一个问题。
排除代码并阅读 HTML 报告
有些代码不值得直接测试,例如调试助手或仅在手动启动脚本时运行的块。 coverage.py 允许您排除带注释的行:
if __name__ == "__main__": # pragma: no cover
main()
您还可以在配置中设置要测量的排除项和源文件夹,例如在 pyproject.toml 中:
[tool.coverage.run]
branch = true
source = ["shop"]
[tool.coverage.report]
show_missing = true
exclude_also = ["if TYPE_CHECKING:"]
谨慎使用排除项并像检查代码一样检查它们。每一个都是一个决定,有些行永远不会被检查。仅仅为了提高数字而排除难以测试的错误处理程序就达不到目的。
对于日常工作,HTML 报告比终端表更有用。运行 coverage html 并打开 htmlcov/index.html。每个文件都以绿色显示已执行的行,以红色显示错过的行,以黄色显示部分分支,因此您可以一目了然地看到哪个 if 从未采取其他路径。许多编辑器还可以在页边空白处显示覆盖范围,这使得在您编写代码时(而不是之后)可以看到间隙。
重构前的覆盖率
覆盖率也是重构前的安全检查。如果您打算重组模块,请先查看其报告。红色区域没有测试来警告您行为是否发生变化,因此在开始之前添加捕获当前行为的特征测试。通过这种方式,覆盖范围不再是一个目标,而是成为一个实用的规划工具。
当错误逃逸到生产环境时,相同的报告会有所帮助。找到错误所在的行并检查是否有任何测试运行了它。如果没有,您就发现了一个值得弥补的差距。如果测试确实运行了它,则问题是弱断言,这正是我们的无断言示例所显示的情况。这两个答案都告诉您如何改进套件。
最后的话
代码覆盖率是一个手电筒,而不是一个等级。使用分支覆盖率来查找没有测试涉及的代码,尤其是错误处理,然后编写做出真正断言的测试。如果您还记得我们运行的一个结果,请将其设为这个:通过一项未检查任何内容的测试达到 100% 的覆盖率。
常见问题
什么是测试中的代码覆盖率?
它是自动化测试运行时执行的代码的百分比,通过行、分支或条件来衡量。它显示了测试触及了什么,而不是他们是否验证了它。
线路覆盖和分支覆盖有什么区别?
线路覆盖检查每条线路是否运行。分支机构覆盖范围检查每个决定是否都是双向的。分支覆盖可以找到线路覆盖遗漏的未经测试的路径。
100% 的代码覆盖率好吗?
不是靠它自己。在我们的测试中,没有断言的套件达到了 100%。旨在对重要代码进行有意义的测试,而不是完美的数字。
什么是好的代码覆盖率?
许多团队的总体目标大约为 70% 到 85%,其中更多的是关键逻辑。断言的趋势和质量比确切的数字更重要。
突变测试与覆盖率有何不同?
突变测试会稍微更改您的代码并检查测试是否失败。它衡量测试是否检测到错误,而覆盖范围无法做到这一点。
继续学习
需要测试、WordPress 或 SEO 方面的帮助?
我们帮助团队交付更好的软件并发展他们的网站。告诉我们您正在做什么,我们将建议明确的下一步。
更多指南 测试、人工智能和现代开发
喜欢这篇文章吗?选择一个主题并直接跳至我们最新的教程。
人工智能幻觉解释:语言模型为何会产生幻觉以及如何减少它
人工智能幻觉解释:它们是什么,为什么语言模型产生它们,常见类型,如何通过基础和提示来减少它们,以及我们进行的简单检查。
阅读文章 →DevOps环境变量解释:.env 文件、秘密和配置正确完成
使用我们运行的 Python 示例解释环境变量:读取和设置它们、为什么每个值都是字符串、.env 文件、进程继承、秘密和验证。
阅读文章 →软件开发API 速率限制解释:算法、标头和处理 429 错误
API 速率限制通过我们运行的模拟进行了解释:令牌桶、固定窗口、滑动窗口、429 响应、速率限制标头、带回退的重试和测试限制。
阅读文章 →测试自动化Python 中的模拟:unittest.mock、补丁以及导致测试谎言的错误
使用unittest.mock 在Python 中进行模拟,经过测试:模拟和return_value、side_effect、修补使用名称的位置、spec 和autospec、模拟日期以及常见错误。
阅读文章 →软件测试代码覆盖率解释:行覆盖率与分支覆盖率以及 100% 的真正含义
用真实的coverage.py运行来解释代码覆盖率:行、分支和条件覆盖率,阅读报告,为什么100%覆盖率仍然会错过错误,以及合理的目标。
阅读文章 →数据库与 SQLSQL 中的 DELETE、TRUNCATE 和 DROP:差异、回滚以及何时使用它们
DELETE、TRUNCATE 和 DROP 通过经过测试的 SQL 进行了解释:各自删除什么、回滚、触发器、身份重置、速度,以及 PostgreSQL、MySQL、SQL Server 和 SQLite 有何不同。
阅读文章 →数据库与 SQLSQL 视图解释:创建、使用和避免陷阱(附示例)
SQL 视图通过我们运行的查询进行解释:创建视图、实时结果、隐藏列、性能、可更新和物化视图、破坏的依赖关系和最佳实践。
阅读文章 →软件开发TypeScript 接口与类型:真正的差异,使用编译器进行测试
TypeScript 接口与类型,使用真实编译器进行检查:两者都可以做什么,只有一个可以做什么,声明合并,扩展与交集以及要遵循的简单规则。
阅读文章 →软件开发Python *args 和 **kwargs 解释:灵活的函数和清晰的示例
Python *args 和 **kwargs 用我们运行的代码进行了解释:打包和解包、参数顺序、仅关键字和仅位置参数、装饰器和常见错误。
阅读文章 →软件开发Python Lambda 函数:语法、实际用途以及何时不使用它们
Python lambda 函数用我们运行的代码进行了解释:语法、使用键排序、映射和过滤器、闭包、后期绑定陷阱,以及何时普通 def 是更好的选择。
阅读文章 →人工智能什么是人工智能代理?它们是如何工作的,只需运行一个简单的代理循环
什么是人工智能代理?关于代理循环、工具、内存和护栏的简单指南,以及我们运行的一个小型 Python 循环、真正的风险以及何时简单的聊天机器人是更好的选择。
阅读文章 →DevOps监控与可观察性:真正的区别,包括日志、指标和跟踪
监控与可观察性解释:已知和未知的故障、日志、指标和跟踪、检查事件演练、黄金信号、OpenTelemetry 和有用的警报。
阅读文章 →DevOpsDocker Volumes 与 Bind Mounts:以正确的方式保留容器数据
Docker 卷与绑定挂载解释:为什么容器数据消失、命名卷、绑定挂载、tmpfs、Compose 示例、备份、权限以及何时使用它们。
阅读文章 →测试自动化TDD 与 BDD:主要区别、以两种方式构建的一个功能以及何时使用每种方式
TDD 与 BDD 的解释是通过一种双向构建的折扣功能:使用我们运行的代码进行红绿重构、Given-When-Then 场景、工具、权衡以及何时使用每种方案。
阅读文章 →软件测试用户验收测试 (UAT):流程、清单和真实示例
用户验收测试解释:谁运行它、它与 QA 有何不同、七步流程、UAT 测试用例模板、清单、签核标准和常见错误。
阅读文章 →数据库与 SQLSQL NULL 值解释:IS NULL、COALESCE、NULLIF 和要避免的陷阱
通过我们运行的查询解释了 SQL NULL 值:三值逻辑、IS NULL、COALESCE、NULLIF,聚合、连接和排序如何处理 NULL,以及它悄悄引起的错误。
阅读文章 →数据库与 SQLSQL CASE WHEN 语句:语法、实例和常见错误
SQL CASE WHEN 解释了我们运行的查询:简单和搜索表单、条件顺序、SUM 透视、COUNT 技巧、自定义排序、更新和要避免的 NULL 陷阱。
阅读文章 →软件开发什么是智威汤逊?通过工作示例解释 JSON Web 令牌
什么是智威汤逊? JSON Web Tokens 解释:这三个部分,在我们运行的代码中构建和篡改的令牌,JWT 与会话,存储,刷新令牌和安全错误。
阅读文章 →软件开发JavaScript 事件循环解释:调用堆栈、微任务和计时器以及示例
JavaScript 事件循环用我们运行的代码进行了解释:调用堆栈、任务和微任务队列、为什么 setTimeout 0 不是立即执行、阻塞、饥饿和面试测验。
阅读文章 →软件开发Python 生成器和 Yield 解释:通过真实示例节省内存
Python 生成器和yield 用我们运行的代码进行了解释:它们如何暂停和恢复、测量内存节省、管道、无限序列、yield from、发送和常见陷阱。
阅读文章 →人工智能什么是嵌入?向量和余弦相似度简单解释
什么是嵌入?关于向量、余弦相似度和语义搜索的简单语言指南,其中包含我们运行的一个小示例、常见用途、选择模型和已知限制。
阅读文章 →DevOps蓝绿部署与金丝雀部署:差异、权衡以及如何选择
蓝绿部署与金丝雀部署解释:每种部署的工作原理、成本、风险和回滚、我们测试的稳定用户分桶、数据库迁移、滚动更新以及如何选择。
阅读文章 →测试自动化不稳定的测试:造成这些问题的原因以及如何彻底解决它们
我们运行的演示解释了不稳定的测试:主要原因、如何确认和诊断、正确的时间修复、共享状态和日期以及隔离过程。
阅读文章 →软件测试负载测试与压力测试:差异、指标和 k6 示例
负载测试与压力测试的解释:各自证明什么、峰值和浸泡测试、重要的指标、为什么平均值会误导、k6 个示例以及如何计划测试。
阅读文章 →软件测试测试用例与测试场景:主要区别、示例以及何时使用它们
测试用例与测试场景的解释:清晰的定义、并排的表格、从场景到案例的密码重置功能、模板以及何时使用每个功能。
阅读文章 →数据库与 SQLSQL 查询优化:读取执行计划并修复慢速查询(带计时)
对一百万行进行 SQL 查询优化测试:读取执行计划、添加正确的索引、修复不可控制的过滤器、使用复合索引和键集分页。
阅读文章 →数据库与 SQLSQL UNION 与 UNION ALL:差异、示例和性能测试
SQL UNION 与 UNION ALL 通过我们运行的查询进行了解释:重复、都遵循的规则、真实的数据错误、测量的性能、INTERSECT 和 EXCEPT 以及何时使用它们。
阅读文章 →软件开发JavaScript Fetch API 教程:GET、POST、错误处理和超时
针对本地服务器测试的 JavaScript Fetch API 教程:GET 和 POST、为什么不抛出 404、可重用的帮助程序、查询字符串、超时、CORS 和常见错误。
阅读文章 →软件开发Python 文件处理:读取、写入、CSV、JSON 和 pathlib 以及示例
使用我们运行的代码解释了 Python 文件处理:打开模式、使用、读取大文件、CSV、JSON、pathlib、二进制文件、安全写入、错误处理和常见错误。
阅读文章 →软件开发Python 类和对象:带有实际示例的 OOP 教程
用我们运行的代码解释 Python 类和对象:__init__、方法、类与实例属性、继承、属性、dunder 方法、数据类和常见错误。
阅读文章 →人工智能什么是大语言模型?法学硕士如何运作,在没有炒作的情况下进行解释
什么是大语言模型?法学硕士用简单的语言解释:标记、嵌入、注意力、训练、温度和上下文窗口、一个微小的下一个单词演示和真正的限制。
阅读文章 →DevOps适合初学者的 Linux 命令:带有示例的基本终端指南
适合初学者的 Linux 命令,按任务分组:导航、文件、查看日志、grep、管道、权限、进程和档案,并附有实用的日志分析示例。
阅读文章 →测试自动化Selenium 与 Playwright:主要区别、代码比较以及选择
Selenium 与 Playwright 比较:架构、等待、浏览器和语言支持、调试、两种工具中的相同测试、迁移建议以及如何选择。
阅读文章 →软件测试等价划分:通过实例解释测试设计技术
等效分区解释:如何找到有效和无效分区,年龄、密码和优惠券的工作示例,我们运行的测试,以及为什么将其与边界配对。
阅读文章 →软件测试软件测试中的严重性与优先级:差异、矩阵和实例
软件测试中的严重性与优先级解释:清晰的定义、带有示例的四向矩阵、级别尺度、谁设置每个级别以及如何运行错误分类。
阅读文章 →数据库与 SQLSQL 子查询教程:类型、相关子查询和 EXISTS 以及示例
SQL 子查询教程,其中包含我们运行的查询:标量、IN、FROM 和 SELECT 子查询、相关子查询、EXISTS、NOT IN NULL 陷阱以及子查询与 JOIN 与 CTE。
阅读文章 →数据库与 SQLSQL 事务和 ACID 解释:COMMIT、ROLLBACK 和隔离级别
SQL 事务和 ACID 通过我们运行的银行转账进行了解释:COMMIT、ROLLBACK、保存点、锁、隔离级别、脏读、死锁和最佳实践。
阅读文章 →软件开发Git merge 与 rebase 的区别:示例及如何选择
用真实的 log 输出、对比表、rebase 黄金法则和安全的团队工作流,讲清 Git merge 与 rebase 的区别。
阅读文章 →软件开发Python 列表推导式:语法、示例与何时避免使用
Python 列表推导式详解:语法、过滤、if-else、嵌套循环、实测耗时,以及何时普通 for 循环更合适。
阅读文章 →数据库与 SQLSQL JOIN 详解:附示例(INNER、LEFT、RIGHT、FULL)
通过示例表和查询,一步步讲解 SQL JOIN:INNER、LEFT、RIGHT、FULL、CROSS 和 SELF,并介绍常见错误与性能技巧。
阅读文章 →