使用变异测试技术评估测试充分性
Back to Top
为了覆盖更广泛的受众,这篇文章已从日语翻译而来。
您可以在这里找到原始版本。
这是is开发者网站 Advent Calendar 2024第3天的文章。
在之前的文章中,曾简单介绍了变异测试,但并未涉及具体的方法。因此,这次以之前的文章中提到的“常见的示例应用”为题材[1],尝试将变异测试引入到Java的开发项目中。
Java的变异测试工具从较早的μJava等偏重于研究的工具发展而来,而最近似乎以PIT或PiTest[2]最为活跃,因此选择使用它。
本文中的代码示例可以在Gitlab 仓库中找到,有兴趣的读者可以一并参考。
开发的假设场景
#提到另一篇文章中的“常见示例应用”,当提出发布审批时,质量管理部门对此提出了异议。
他们表示:“既然发现了如此不稳定的现象,那么是否应对测试的充分性进行严格评估?”
基于此,决定通过评估测试是否能可靠地检测出实现中的错误,以此来证明测试的健全性。这里使用所谓的“变异覆盖率”进行健全性定量评估。
什么是变异测试
#变异测试并不是评价测试对象,而是评价所实施的测试是否合理的一种方法。
可以考虑测试一个常见的示例程序“FizzBuzz判定[3]”。在测试这个程序时,验证到什么程度的模式才能算是足够呢?
如果要断言“无论任何前提都完全没有问题”,需要检查所有可能的输入模式,这显然是不现实的。因此,通常会采取一种理论上的方法,例如,“检查3、6、9……等3的倍数中具有代表性的6是否可以验证Fizz,进而推断其他模式也没有问题”[4]。
变异测试通过评估“对编写的程序中发生的错误,测试是否能够检测出来”的方式来判断测试是否充分。例如,如果“应该除以3的地方错误地改为除以2,并在能够被整除的情况下返回Fizz”,“6可以正确返回Fizz,但9不会返回Fizz”,这表明测试可能存在不足[5]。
为了引发“程序的错误行为”,工具会机械地改写程序的行为,然后在改写后的状态下运行测试。如果测试失败,则可以评价为该测试能够检测出发生的错误。通过对各种改写进行测试,如下图所示(※),可以获得测试可能不充分的错误(改写)模式信息。
※:想必您已猜到,此图仅为示意,图中所示代码为伪代码,不是真正可运行的代码。
引入PIT(PiTest)
#PiTest有一个可以独立运行的命令行工具,同时也为Ant/Maven/Gradle等构建工具提供插件功能[6]。
开发的“常见示例应用”使用Gradle作为构建工具,因此我们决定使用其插件。虽然插件会承担大部分基础配置,但如果测试框架使用JUnit5,还需要为Gradle的pitest任务配置JUnit5相关设置。
如果不需要设置具体条件,只需在build.gradle中新增如下插件配置,就可以执行PiTest。
plugins {
id 'java-library'
・・・
// 在Gradle设置中添加PiTest插件
id 'info.solidsoft.pitest' version '1.15.0'
}
pitest {
// 使用JUnit5时,为进行变异测试需对pitest任务指定JUnit5插件
junit5PluginVersion = '1.1.2' //or 0.15 for PIT <1.9.0
// 根据需要新增其他配置
}
通过Gradle插件,pitest
任务会添加到Gradle的任务列表中。通过执行此任务,可以根据配置执行变异测试,并在Gradle的构建输出路径(build/reports/pitest)中生成测试结果报告。
PiTest支持与主流的Mock框架兼容共用,至少在本文使用的示例应用场景中,也可以与Allure、Pact等测试工具结合使用。
结果评估与测试补充
#报告会显示变异测试的汇总信息,通过汇总的信息可以逐层深入,查看变异测试的具体详情。
汇总信息展示:
- Line Coverage:覆盖条件分支等测试对象时,测试覆盖的程度(通常的行覆盖率)。
- Mutation Coverage:针对工具添加的改写(变异),“测试失败”(KILL)并检测出的比例。
- Test Strength:估测已执行测试对变异检测能力的程度[7]。
详细信息:
- 浅绿/红色标记的代码:测试覆盖/未覆盖的代码行(通常的行覆盖率)。
- 深绿/红色标记的代码:能够检测(KILL)/无法检测变异的行。
- 变异详情信息:每个变异点的改写内容及结果。
例如,根据上述结果,针对第26行if (4 <= hour && hour < 11)
的条件,2种边界值更改(如4 < hour
及hour <= 11
)和判定反转(如4 > hour
及hour >= 11
)的变异中,有3/4未被检测到。这表明,与此相关的测试可能不足,“即使存在错误也未被检测到”。
检查对应的测试代码后确认,目前仅测试了普通问候的一个用例。在这种状态下,将来一旦出现错误,测试可能会检测不到,所以建议补充在条件边界附近的测试。
针对这些变异,为确保测试失败,可以聚焦于条件边界值进行补充测试,从而提高变异覆盖率。无意中,这个示例还帮助发现“等价类划分”和“边界值分析”的不足之处,并加以改进。这样一来,也能够更有依据地对质量管理部门说:“测试合理且充分!!”
总结
#本文简单介绍了变异测试的概念以及测试工具PIT(PiTest)的基础用法。尽管引入成本较低,变异测试却能为测试质量提供有价值的信息。可以说,它展示了无尽的潜力。
关于如何更有效地将其融入开发,留待以后再进行详细介绍。
是的,抱歉。当初原本想在这篇文章后再写相关内容,但后来由于忙于各种事情而完全忘记了,结果不得不在另一篇文章中说“变异测试很方便”之类的话。这次决定不再拖延,认真把这个内容补上。 ↩︎
乍一听名字,会觉得“为什么是Java却用这个名字?”似乎会令人困惑,但实际上它与PyTest仅是名字偶然相似,并无关联。据说名字的来源是作为并行(Parallel)与独立(Isolated)执行JUnit测试(Parallel isolated Test)的项目,后来扩展为需要并行执行(例如变异测试)的其他技术。因此,从缩写命名来看,说成“pi(っ)测试”也不无可能。 ↩︎
为了说明,对数值输入进行以下操作:如果能被3整除,输出Fizz;如果能被5整除,输出Buzz;如果同时能被3和5整除,输出FizzBuzz;否则,返回输入值。FizzBuzz据说在英语国家是一种打发时间的游戏,不过作者没玩过,所以不清楚这种玩法是否有趣。 ↩︎
这是一种以“测试被合理设计”为前提,判断测试充分性的方式。作者个人非常喜欢这种方式,但有时会因为需要让对方理解设计的合理性而感到一些难点。(不过,这些讨论反而能增强测试的质量,这仍是件好事……) ↩︎
请注意,这并不是说“因此需要用6和9进行测试”。重点在于整体测试时,评估是否为“若存在错误则能够被检测的测试”,用于判断测试是否合理。 ↩︎
官方文档表明,Gradle插件似乎是由第三方提供的。虽然最近的更新频率稍显缓慢,但由于pitest本身变化不大,使用时似乎不会有太大问题。此外,还有一个带扩展功能的付费版本arcmutate,但本文暂不讨论。 ↩︎
根据作者的研究,未能在官方文档中找到确切定义,但根据解析结果,看来是基于“测试KILL的变异数”/“被测试的变异数”计算得出的信息。与Mutation Coverage的主要区别在于母数是否包含“创建的所有变异”。直观来看,Test Strength的评价更倾向于“已执行测试”的能力。 ↩︎