谁将自己测试这些测试?(2017)

2020-09-08 03:46:16

在这篇文章中,我将关注突变测试。这是针对那些不熟悉突变检测的人。我将介绍什么是突变测试;展示一些它可以做什么的示例;考虑它的局限性,看看如何使用PIT将突变测试添加到Java项目中。我在这篇文章中使用的例子可以在这个库中找到。

非常简单,突变测试是一种通过在代码中引入更改并查看测试套件是否检测到这些更改来测试测试质量的方法。

我认为看待这个问题的最好方式是直接举个例子。假设您刚刚编写了以下Java代码:

私有静态最终int margarine_weight=100;私有静态最终int cocoa_weight=25;私有静态最终int egg_count=2;私有静态最终int range_juce_volume=15;Cake createCake(CakeType CakeType){Cake Cake=new Cake();Cake。人造黄油(Margarine_Weight);蛋糕。凝糖(人造黄油重量);蛋糕。SetEggs(Egg_Count);IF(CakeType.。巧克力味的。等于(CakeType)){蛋糕。SetFlour(人造黄油重量-可可重量);蛋糕。SetCocoa(可可重量);}否则{蛋糕。SetFlour(Margarine_Weight);if(CakeType.。橙色的。等于(CakeType)){蛋糕。SetOrangeJuice(Orange_Juice_Volume);}}退还蛋糕;}。

这是一个相当简单的工厂方法,用于创建填充了相关配料字段的Cake对象。此外,您还创建了以下测试:

@Test public void canCreateVictoriaSponge(){Cake Actual=被测试者。CreateCake(CakeType.。Victoria_Spenge);assertEquals(100,Actual。GetMargarine());assertEquals(100,Actual。GetFlour());assertEquals(100,Actual。GetSugar());assertEquals(2,Actual。GetEggs());assertEquals(0,Actual。GetOrangeJuice());}@Test public void canCreateChocolateCake(){Cake Actual=被测试者。CreateCake(CakeType.。巧克力);assertEquals(100,实际。GetMargarine());assertEquals(25,Actual。GetCocoa());assertEquals(100,Actual。GetSugar());assertEquals(2,Actual。GetEggs());assertEquals(0,Actual。GetOrangeJuice());}@Test public void canCreateOrangeCake(){Cake Actual=被测试者。CreateCake(CakeType.。橙色);assertEquals(100,实际。GetMargarine());assertEquals(100,Actual。GetFlour());assertEquals(100,Actual。GetSugar());assertEquals(2,Actual。GetEggs());assertEquals(15,Actual。GetOrangeJuice());}。

这一切都很棒,您有针对每种不同类型蛋糕的测试场景,并且您已经运行了代码覆盖工具,并且您拥有100%的行覆盖率。因此,您会有一种温暖而模糊的感觉,因为您知道您的功能已经过全面测试,如果它在将来被破坏,测试将无法向开发人员发出警报。但是,这种信心是不是放错了地方呢?你们中比较精明的人会意识到,如果不是故意的错误,我不会包含这个示例,所以让我们来看一下突变测试运行对此代码的影响:

正如您可以看到的,突变测试突出显示了设置巧克力蛋糕面粉数量的行,当您回顾测试时,果然缺少面粉上的断言。高亮显示的代码行可以更改,甚至可以删除,所有测试仍将通过,直到某个不幸的用户尝试制作不含面粉的巧克力蛋糕时才会知道。

诚然,这是一个简单的人为设计的示例,将测试中重复的断言提取到帮助器方法中可能会使错误变得显而易见,但我确信您可以想象更复杂的真实世界场景,其中代码由测试执行,而不是实际断言。

突变测试的工作原理是,既然您的测试代码在那里是为了确保您的部署代码做正确的事情,那么如果部署的代码被更改为做其他事情,那么至少应该有一个测试失败。突变测试工具将向您部署的代码引入突变,然后对其运行测试套件。如果一项很棒的测试失败了,突变就会被杀死,但如果所有的测试都通过了,那么突变就会存活下来,这表明有一个潜在的领域可能会引入细菌。

为了回答这个问题,我将更详细地查看我正在使用的工具。在这些示例中,我使用的是PIT,它使用ASM操作内存中的字节代码,并使用插装API将突变插入到JVM中。还有其他使用不同方法的工具,在IT的网站上有更详细的讨论。

这份测试报告全是绿色的,也就是说所有的突变都被杀死了。代码行旁边的蓝色数字表示该行代码生成了多少个突变。在突变部分的代码下面,它详细说明了为每一行引入的所有突变,以及它们是否被杀死。在这个例子中,有5种不同的突变,让我们来看看这些突变。

此突变将关系运算符更改为添加或删除等号,从而有效地将边界移位1。示例中的突变是。

这一突变将使条件反转,使其与最初的行为背道而驰。示例中的突变是。

这种变化删除了整个条件语句,并将其替换为false,因此永远不会执行if块中的代码。示例中的突变是。

此变化与前一个变化相同,只是条件被替换为true,因此始终执行if块。示例中的突变是

这种突变需要更多的解释。它有效地改变了RETURN语句,使其返回NULL以确保对返回值进行断言。但是,有时正确的返回值可能为NULL,因此在这些情况下,代码会发生变化,抛出异常。因此,在示例中,第13行(return";Zero&34;;)将变为如下所示:

这种变异还可以应用于返回原始值而不是返回NULL的方法,在这些情况下,它会返回不同的原始值。

这个例子只演示了一只手,里面装满了潜在的突变。其他变量包括:-更改算术运算-删除对方法的调用-更改switch语句中的缺省块。

因此,这将更改代码的每一行,然后针对它运行整个测试套件,这不是要花费很长时间才能运行吗?

虽然这个过程确实不是很快,但是PIT做了很多事情来优化性能,所以它不会像您想象的那样需要很长时间。

First Off PIT不会针对每个突变运行整个测试套件。它首先进行快速的线路覆盖分析。这使它能够知道哪些测试执行给定的代码行。然后,它将只运行针对突变实际执行给定代码行的测试。在此基础上,PIT将优先进行速度更快的测试,并且在与标准命名约定匹配的测试类中,例如,对于foo中的突变,它将优先选择FooTest或TestFoo中的测试。

除了PIT固有的优化功能之外,还有其他配置选项。在减少分析时间方面,最有效的方法可能是启用增量分析。启用此功能后,PIT将创建一个历史文件,然后在随后的运行中,它将只分析更改的文件。

通常,您只对当前正在处理的类感兴趣,所以您不想对整个代码库运行分析。PIT允许您添加筛选器来挑选您想要包含的特定包或类。它也有一种更少手动的方式来限制分析的范围,因为它可以配置为绑定到您的版本控制系统中,并且只分析已添加或修改的源文件。

另一种加速测试运行的有效方法是(假设您有一个多核处理器)简单地增加PIT使用的并行线程的数量。

在循环中生成突变时,极有可能会导致无限循环。由于这个原因,PIT有一个超时时间,通常可以安全地假设任何超时的突变都会被您的测试拾取,因为您不会在测试套件永远运行的地方提交代码。

从理论上讲,在创建突变时,PIT可能会产生无效的字节码,但我还没有遇到过这种情况。此外,它应该只影响个体突变,并允许其余的分析继续进行。

是的,有时您有一些代码,您无法或没有逻辑意义地在单元测试级进行测试。可以理解的是,您不希望突变测试占用分析时间,并且会因为失败而使结果变得模糊。

最明显的例子是日志记录代码,通常您不会太关心日志记录输出的确切细节。幸运的是,PIT也考虑到了这一点,默认情况下,它将识别来自标准日志记录库的日志记录代码,并且不会在这些行中创建突变。请参阅以下示例:

正如您在本例中看到的,第14、44和45行上存在日志记录方法,但是这些行上没有生成任何突变。但是,第45行仍然突出显示。不幸的是,因为我的测试没有使用跟踪级别日志记录来运行,所以该行从未执行过,所以它被突出显示为未覆盖的代码。

此外,可以将PIT配置为根据需要排除代码段。这可以通过指定应该排除的类或方法名来实现。或者,可以使用与标准日志记录包调用相同的方式排除调用特定包或类的行。这不会在配置的代码行中产生突变,但它仍然会突出显示从未在任何测试中执行的代码行。

除了验证测试质量的核心目标之外,还有其他几个好处。首先,它可以识别冗余码。考虑以下示例:

第18和19行被突出显示为允许突变存活。这不是由于测试中的缺陷,而是那些代码行不是必需的。由于culteIfAbsend返回现有值(如果存在),因此如果删除第18行上的初始GET,代码将进入If块并获取第20行上的相同结果。类似地,当If条件突变为Always TRUE时,该值将从映射中提取两次,但它仍然是相同的值,因此净结果将是相同的。

PIT特有的一个优势是,因为它拆分测试并将它们分组运行,所以可以揭示不需要的测试顺序依赖关系。

与评估测试质量的任何度量一样,焦点可能会变成任意的统计数据。与代码覆盖率相比,突变测试提供了更好的测试质量度量,但良好的突变覆盖率仍然不能保证您的代码是正确的和经过良好测试的。但是,它是代码中易受引入错误影响的区域的有用指示器,因为当行为更改时没有测试失败。

在某些情况下,突变检测会标记出根本不存在的问题。

上面的示例显示了一个等价的突变,因为当行发生突变时,代码仍然会产生相同的结果。要查看第8行中发生的情况,我们需要查看下一个值与当前最低值相同时发生的情况。在未突变的代码中,lowest.compareTo(I)>;0解析为false,因此最低值保持不变。当代码发生变化时,条件变为lowest.compareTo(I)>;=0,因此现在当值相同时,条件的计算结果为真,重新分配最低值。但是,因为这两个值相同,所以结果没有改变,所以突变被标记为存活。没有办法避免这些误报。这是一个相当简单的示例,但可能会有更复杂的场景,例如,如果您编写了不更改最终结果的代码来提高性能。

另一个存活突变的潜在场景是,您故意包含无法访问的代码。还记得我们一开始使用的蛋糕工厂方法吗?在这个例子中,如果蛋糕类型不是巧克力或橙子,那么它就给出了维多利亚海绵的原料。因为这是仅有的3种受支持的蛋糕类型,所以这可以很好地工作,但是如果您想要添加一种防止添加新类型的蛋糕的保护措施,您可以将代码重构为如下所示:

现在,我们有了一个带有默认块的switch语句。如果在没有配置的情况下添加新的蛋糕类型,那么代码很快就会失败。然而,就目前而言,所有可能的场景都不会执行缺省块中的代码,因此那里的所有突变都会继续存在。

如果您有一个Maven项目,那么要运行PIT,您至少需要将插件添加到您的pom文件中:

这将在./target/PiT-Reports中创建一个突变复盖率报告,这将提供一个总体复盖率摘要,但是您还可以深入到包和特定的类。

当然,您可以添加其他配置,其中一些我在前面已经讨论过了。下面是我用来生成所有这些示例的存储库中的配置。

<;plugin>;<;groupId>;org.picest<;/groupId>;<;artifactId>;piest-maven<;/artifactId>;/artifactId>;Version>;/version>;<;Executions>;<;Execution>;<;目标>;<;版本>;1.2.3<;/Version>;<;Executions>;<;Execution>;<;Goals>;<<;History InputFile>;./PitHistory.bin<;/History yInputFile>;<;/History yOutputFile>;/PityOutputFile>;<;excludedClasses>;com.mutation.testing.demo.ExcludedFromAnalysis<;/excludedClasses>;<;excludedMethods>;unusedMethod<;/excludedMethods>;<;线程>;8<;/<;/变种人>;<;/configuration>;<;/Execution>;<;/Executions>;<;/Plugin>;

此配置包括maturationCoverage目标,因此它将作为构建的一部分执行,而不需要独立运行。指定的配置参数如下:

History yInputFile/History yOutputFile-这些值一起激活增量分析。它们指定历史文件的位置。这些位置通常是相同的,因此只对自上次运行以来的更改执行分析。

ExcludedClasses-这是将从分析中排除的类的逗号分隔列表。*可以作为通配符指定整个包。

ExcludedMethods-这是将从分析中排除的方法名称的逗号分隔列表。具有匹配名称的任何方法都将被排除,而不管它所在的类是什么。

线程-如前所述,这是要使用的线程数量,如果您的处理器有足够的内核,这将加快分析速度。

变异器-这指定了默认情况下,PIT只启用了一些变异器,并将哪些变异器应用于您的代码。默认配置集中于不易检测且不太可能产生等效突变的突变。

这些只是我在此演示项目中使用的配置选项。完整的细节可以在PIT的网站上找到。

酷,最后一件事,对于那些不想看整篇文章的人来说,你能用一个方便的段落总结一下这篇文章吗?

是的我可以。突变测试是一种度量测试质量的方法,它不仅检测代码是否执行,而且评估测试套件检测代码更改的能力。它通过在代码中引入称为突变的更改,然后查看该突变是否会导致测试失败来做到这一点。这有时会由于等价的突变而错误地标记代码行,其中突变的代码行为不同,但最终结果是相同的。如果您使用的是Java,那么PIT提供了一个易于使用的工具,该工具运行相对较快,可高度配置,并产生用户友好的输出。