最近,我反复提到“ 突变测试 ”一词。 因为可以说这种方法能够以超出代码覆盖范围的方式检测测试安全网的空白,所以我花了一些时间来追赶这个话题,然后尝试一下。 这篇文章总结了我的发现,作为对该主题的快速介绍。
什么是变异测试?
变异测试评估现有软件测试的质量。 想法是以较小的方式修改(变异)测试所覆盖的代码,并检查现有测试集是否将检测并拒绝更改[MUTTES]。 如果不符合,则意味着测试不符合代码的复杂性,并且未测试其一个或多个方面。
在Java中,将变量视为与原始代码相比具有单个修改的附加类。 可能是如下所示的if
子句中逻辑运算符的更改。
if( a && b ) {...} => if( a || b ) {...}
通过现有测试检测并拒绝这种修饰称为杀死突变体。 当然,有了完善的测试套件,没有任何类别的突变体能够生存。 但是创建所有可能的变体的成本很高,这就是为什么在现实世界中手动执行此方法不可行的原因。
幸运的是,有一些工具可以即时创建突变体,并针对每个突变体自动运行所有测试。 变异创建基于一组所谓的变异算子 ,这些变异算子可以揭示典型的编程错误。 在上面的示例中将采用的一个称为条件突变算子 。
使用JUnit进行测试
使用JUnit进行测试是Java开发人员可以学习的最有价值的技能之一。 无论您的背景是什么,无论您是只是想建立一个安全网以减少桌面应用程序的性能下降,还是要基于健壮且可重用的组件来提高服务器端的可靠性,都需要进行单元测试。
弗兰克(Frank)写了一本书,为使用JUnit进行测试的基本知识提供了深刻的切入点,并为您准备了与测试有关的日常工作挑战做好了准备。
学到更多…
它与代码覆盖率有何关系?
正如Martin Fowler所说的那样, “测试覆盖率是查找代码库中未经测试的部分的有用工具 ”。 这意味着覆盖率不佳表明测试套件的安全网中存在令人担忧的漏洞。 但是,仅覆盖范围就不能证明基础测试的质量! 得出的唯一合理结论是,显然没有发现斑点。
为了澄清这一点,例如,考虑一组测试,这些测试完全省略了验证阶段 。 尽管这样的捆绑包可能会实现完整的代码覆盖,但是从质量保证的角度来看,这显然是毫无用处的。 这就是突变测试起作用的地方。
测试套件杀死的突变体越多,生产代码的行为被良好构想并被可靠测试完全覆盖的机会就越大。 听起来诱人? 然后,让我们继续看一个例子,以了解实际应用。
如何使用?
我们从我从《 用JUnit测试》一书中借来的清单开始,然后针对实际上下文对其进行一些修改。 例如,可以将时间轴视为UI控件的模型组件,该控件可以按时间顺序显示列表条目,例如Twitter界面。 在此阶段,我们只关心状态变量fetchCount
,其初始值可以通过正整数来调整。
public class Timeline {static final int DEFAULT_FETCH_COUNT = 10;private int fetchCount;public Timeline() {fetchCount = DEFAULT_FETCH_COUNT;}public void setFetchCount( int fetchCount ) {if( fetchCount <= 0 ) {String msg = "Argument 'fetchCount' must be a positive value.";throw new IllegalArgumentException( msg );}this.fetchCount = fetchCount;}public int getFetchCount() {return fetchCount;}
}
虽然这里没有什么复杂的,但是我们对下面的测试用例感到放心(让我们使用JUnit内置的org.junit.Assert
类的各种assert方法来进行验证,在这篇文章中使用了静态导入来简化内容) )。
public class TimelineTest {private Timeline timeline;@Beforepublic void setUp() {timeline = new Timeline();}@Testpublic void setFetchCount() {int expected = 5;timeline.setFetchCount( expected );int actual = timeline.getFetchCount();assertEquals( expected, actual );}@Test( expected = IllegalArgumentException.class )public void setFetchCountWithNonPositiveValue() {timeline.setFetchCount( 0 );}
}
确实,在使用EclEmma收集覆盖率数据的同时运行测试会产生完整的覆盖率报告,如下图所示。
可能您已经检测到了弱点。 但是,让我们天真地玩,忽略地平线上的乌云,然后继续进行突变测试。 我们将PIT用于此目的,因为它似乎是该领域中最受欢迎和最活跃的工具。 其他可能性包括µJava和Jumble 。
PIT支持命令行执行 , Ant和Maven构建集成以及第三方产品的 IDE和报告集成。 有关各种使用方案的更多详细信息,请参阅相应的在线文档。
生成的针对特定项目的变异测试HTML报告包含程序包细分,并且可以深入到类级别。 下图显示了时间轴组件的类列表报告。 下面,同一报告在Eclipse IDE中显示为结构树。
太震惊了! 我们对高覆盖率的信心是一种错觉。 如您所见,该报告列出了将哪些突变应用于哪一行。 同样,请记住,对于每个突变,将执行一个单独的测试运行,包括所有测试! 带绿色下划线的列表条目表示被杀死的突变体,而红色的表示幸存者。
仔细检查,很快就会知道我们错过了什么。 我们通过在测试用例中添加初始状态验证来解决此问题,如以下代码片段所示(请注意,静态导入Timeline.DEFAULT_FETCH_COUNT
)。
public class TimelineTest {[...]@Testpublic void initialState() {assertEquals( DEFAULT_FETCH_COUNT, timeline.getFetchCount() );}[...]
}
就是这个! 现在,突变测试运行会杀死所有突变体。 下图显示了一份报告,其中列出了所有报告。
很难相信为这么小的一类人创造的突变数量。 9个突变体,仅需22条指令! 这将我们引到本文的最后一部分。
缺点是什么?
上游覆盖率分析,动态创建突变体以及所有必要的测试运行都需要花费大量时间。 我将突变测试纳入了完整的时间轴示例应用程序的构建过程中,该应用程序包含一个包含约350个测试的套件。 与常规运行相比,这将执行时间增加了四倍。
有了这些数字,很明显,出于实际原因,变异测试运行不能像单元测试运行那样频繁地执行。 因此,找到合适的工作流程以在早期反馈和效率方面提供最佳折衷是很重要的。 对于大型软件系统,这可能意味着突变测试运行可能更好地限于夜间构建等。
现场测试中出现了另一个问题,表明PIT可能会遇到基础技术堆栈[STAPIT]的麻烦。 就我而言,似乎不支持用于基于枚举的参数化测试的Burst JUnit 测试运行器 。 因此,特定类别的所有突变都可以幸免。 但是手动复制证明了这些结果是错误的。 因此,您要么不用麻烦的技术,要么将PIT配置为排除麻烦的测试用例。
摘要
这篇文章简要介绍了突变测试。 我们已经了解了什么是测试突变体,突变体的杀死率如何说明现有测试套件的质量,以及该测试技术与代码覆盖率之间的关系。 此外,我们已经了解了如何使用该领域最受欢迎的工具PIT,并对一些执行报告进行了评估。 考虑到从现场测试中得出的一些缺点,得出了本主题的结论。
总之,变异测试似乎是对基于自动化测试的质量保证工具集的有趣补充。 正如开头提到的那样,我对这个话题还很陌生,因此,从更高级的用户那里得知他们可能错过或遗忘的经验和方面会很有趣。
参考文献
- [MUTTES]:突变测试,维基百科, https ://en.wikipedia.org/wiki/Mutation_testing
- [STAPIT]:JUnit测试通过了,但是…,Stackoverflow, http ://stackoverflow.com/questions/30789480/
- [TESCOV]:TestCoverage,Fowler, http ://martinfowler.com/bliki/TestCoverage.html
翻译自: https://www.javacodegeeks.com/2015/10/what-the-heck-is-mutation-testing.html