本主题说明改进单元测试覆盖率的策略。

章节目录:

了解低覆盖率的原因

C++test 的测试覆盖率是通过几种预定义的指标来衡量的(如查看覆盖率信息中所述)。通常,“提高测试覆盖率”的目标应是提升所有测试覆盖率指标。尽管这些指标会分别受到本文所述技巧的影响,但在每种情况下,都可以在特定技巧的应用与其对某一覆盖率指标的影响之间找到简单的逻辑联系。因此,为简化讨论,主要针对行覆盖率进行技巧说明,对其他指标的扩展则不进行详述。

增加测试覆盖率的问题通常可以总结为以下三种情况:

  1. 现有测试未使用能够触发代码中条件语句的控制表达式以执行所有分支的输入值。
  2. 被测函数中的控制表达式依赖于其他函数的返回值,导致控制表达式依赖于测试执行时程序/代码的状态。
  3. 现有测试未对被测代码进行正确设置,因此运行测试会导致异常,从而在异常发生的位置中断执行流程。

根据代码的结构,控制表达式可能很简单(例如,每个函数仅包含一个 if/else 语句),也可能非常复杂(例如,嵌套循环条件,其中控制表达式是函数调用的返回值,与分支表达式交错在一起)。

基于上述情况, 在 C++test 框架中针对每种情况都有适用的条件控制技巧。

增加代码覆盖率的策略

增加代码覆盖率的一般技巧如下:

我们建议按照以下步骤来改进测试覆盖率:

  1. 检查相关作用域(项目或文件)的代码覆盖率。
  2. 如果代码覆盖率低于期望水平,分析统计数据以对文件或函数进行排序,主要基于
    • 最低代码覆盖率,或
    • 通过检查被测代码判断出的增加相应覆盖率的潜在回报。
  3. 根据所有按序排列的函数,对所有阻碍覆盖率及其条件值的控制表达式执行以下步骤:
    1. 如果条件语句是一个直接的函数参数或一个函数类的数据成员,则添加测试用例,应用特定输入值使控制表达式求值为期望的分支。
    2. 或者,如果条件是函数的直接参数或类数据成员的简单运算结果,则添加测试用例创建测试对象,并将数据成员和测试函数的输入参数设置为特定值,从而使控制表达式求值为期望的分支。
    3. 或者,如果控制表达式似乎依赖于某个复杂对象(通过方法调用),则创建一个处于适当状态的复杂测试对象(请参阅下文的复杂对象)
    4. 或者,如果覆盖率不足是由于异常中断了执行流程:
      • 检测代码找出抛出异常的原因
      • 如果抛出异常是由不正确的函数/测试用例参数值(空指针解析引用等)导致的,则创建/修改一个可以将正确值传给函数的测试用例。
      • 如果指定对象的状态有问题,请参阅下文的复杂对象。
      • 为抛出异常的函数创建一个用户桩函数。
    5. 或者,如果条件语句是在被测函数内部通过复杂的代码序列计算得出的,则继续处理下一个函数。

测试驱动的桩函数通常适用于没有或只有少量前提条件或参数的函数,也适用于封装了 UI 交互并返回表示用户操作的值的函数。例如 GUIWidget::whichButtonWasPressed() 函数。

如果条件语句是函数的一个返回值,符号替换的优先顺序为:a) 原始函数 b) 用户桩函数。

用户桩函数

通常编写用户桩函数以返回下列值之一:

  • 每次调用时相同的值
  • 每次调用时不同的值
  • 取决于测试用例名称的值(测试驱动的桩函数)

复杂对象

如果条件语句依赖于复杂对象的状态(例如,对列表中多个成员执行操作的函数,例如 List::containsElement(Element&)),那么在函数测试之前,需要将对象置于适当的状态。此时有两个重要的考虑因素:

  1. 对象的期望状态是什么?
  2. 如何实现该状态?

只要理解了第一部分,C++test 测试用例中对象的期望状态一般可以通过以下方法实现:

  • 使用逐个为对象的成员进行初始化(仅对简单类有效)。借助 C++test 插桩,可以从测试用例的主体直接访问对象的所有私有数据成员。因此,您可以通过直接赋值给数据成员来影响特定测试用例的执行。
  • 使用具有指定参数集的参数化构造函数来创建测试对象。
  • 使用在测试套件的 setUp 方法中应用的指定初始化调用序列。当测试对象总是需要非平凡的初始化时,这种方法特别有效。使用 setUp 方法可以一次指定初始化序列,并自动将其应用于测试套件的所有测试用例中。
  • 使用测试对象工厂,通过工厂类方法提供已知状态的测试对象。

使用覆盖率指导助手增加覆盖率

覆盖率指导助手可以帮助您有效地实施上述增加代码覆盖率的策略。它可以分析代码以计算覆盖未被覆盖的代码行所需的测试用例前提条件,从而帮助您创建用户定义的测试用例。详细信息,请参阅使用覆盖率指导助手

  • No labels