一、Mutation Testing(以下简称MT)是什么?
通俗的讲就是UT的UT
MT修改部分源代码后,在改变后的源代码上运行UT,再比较修改前后的差别,检测UT是否覆盖了各种场景。
自己理解就是针对某些误写错误的,比如>=误写成了<=,UT可能覆盖不到,但MT可以覆盖到。
最重要的它能够提高UT的准确度,可以检测出即使UT覆盖100%,但是某些逻辑判断仍然无法覆盖到的情况,更有效的降低了bug出现的概率。
二、MT的原理
1.判断
大于变大于等于,具体如下:
原始逻辑 | MT修改后逻辑 |
---|---|
< | <= |
<= | < |
> | >= |
>= | > |
//源代码
if(a
2.计算
原始逻辑 | MT修改后逻辑 |
---|---|
+ | - |
- | + |
* | / |
/ | * |
% | * |
<< | >> |
>> | << |
>>> | << |
//源代码
int a = b + c;
//被MT修改为
int a = b - c;
3.void方法
MT会直接注释掉某些void方法使之不运行
//源代码
public void someVoidMethod(int i){
//编码
}
public int foo() {
int i = 5;
someVoidMethod(i);
return i;
}
//被MT修改为
public void someVoidMethod(int i){
//编码
}
public int foo(){
int i =5;
return i;
}
4.更多规则
更多规则可见,https://pitest.org/quickstart...
5.具体MT操作
MT的每一次代码修改都会生成一个Mutant,每一个UT会针对每一个Mutant运行,将运行结果与原始的代码的UT结果比较,如果修改后的UT结果和修改前的一致,就表明代码覆盖不够全面,会记录为Survive,如果结果不一致,记录为Killed,表示UT覆盖的比较全。类似于下两图的操作:
三、MT的配置
项目pom.xml增加以下配置,threads可以增加多线程运行MT,以降低运行时间;timestampedReports默认为true,每次运行MT都会生成一个时间戳的文件夹,修改为false表示直接在设置的根目录生成报告,不再创建时间戳文件夹
org.pitest
pitest-maven
1.6.3
10
**.*IT
xml
html
src/test/resources/mutationHistory
src/test/resources/mutationHistory
false
如果只修改了部分代码,需要单独运行MT,在以上配置中的configuration中增加以下内容,可以单独指定运行某个class的MT
com.a.b.c.AServiceImpl
com.a.b.c.AServiceImplTest
如果需要修改更加严格的MT测试,可以在configuration中增加以下内容,其中可以修改的油DEFAULTS,STRONGER,ALL,具体规则见https://pitest.org/quickstart...
DEFAULTS
执行命令进行MT
mvn org.pitest:pitest-maven:mutationCoverage
本地运行MT,如果代码UT以及源代码较多,会耗时很久,因此需要通过上面的配置historyOutputFIle,每次执行完毕会生成历史文件,下次再次运行MT,只会运行有源代码变化的UT。缩短后期每次的执行时间。具体如下截图,内容是
四、运行完成的报告
五、我在针对MT测试的情况下如何写UT
覆盖要全,各个分支都要覆盖到,其实正常咱们自己写UT时候也应该默认覆盖到(KILLED),如果没有覆盖到会显示(SURVIVED)
//源代码
public String testMT (String code) {
if(code.equals("1")){
return "SUCCESS";
}
return "FAIL";
}
//UT
@Test
public void testMT(){
Assert.assertEquals("SUCCESS",mtDemo.testMT("1"));
Assert.assertEquals("FAIL",mtDemo.testMT("2"));
}
如果某些逻辑情况下是throw出XXException时候,需要在test注解中增加以下内容,也能够覆盖MT@Test(expected=XXException.class)
源代码中尽量返回有意义的结果,避免使用void,返回void时,MT强行注释掉的情况,无论如何都无法通过断言去判断(其实感觉这种情况就是模拟在写代码的时候,完成了编写void的方法后,但是忘了在主方法中调用了的情况)。
//明确的返回可以断言判断
Assert.assertEquals("SUCCESS",mtDemo.testMT("1"));
if判断中有多个条件的,mock返回的需要覆盖到最后一个判断,下面图中显示支付和param2为1,但是param1不符合的时候就过了,MT就认为覆盖不够全main,但是UT修改后增加另外一种情况param1符合,param2不符合,正好完美覆盖,MT则表示Kill,提示覆盖完全
//源代码
public String testIf(String param1,String param2){
if("1".equals(param1) || "1".equals(param2)){
return "SUCCESS";
}else{
return "FAIL";
}
}
//UT
@Test
public void testIf(){
Assert.assertEquals("SUCCESS",mtDemo.testIf("0","1"));
}
@Test
public void testIf(){
Assert.assertEquals("SUCCESS",mtDemo.testIf("0","1"));
Assert.assertEquals("SUCCESS",mtDemo.testIf("1","0"));
}
还有一些复杂的情况,例如lambda,实际上也是要覆盖到不同的情况返回不同的结果
//源代码
static class TestDto{
private String name;
private String type;
TestDto(String name,String type){
this.name = name;
this.type = type;
}
public String getName(){
return this.name;
}
}
public List testLambda (List list) {
return list.stream()
.filter(l-> l.getName().equals("name1"))
.collect(Collectors.toList());
}
//UT,只覆盖了name1存在的情况,如果name1不存在情况没覆盖
@Test
public void testLambda(){
List list = new ArrayList<>();
MTDemo.TestDto dto1 = new MTDemo.TestDto("name1","type1");
MTDemo.TestDto dto2 = new MTDemo.TestDto("name2","type2");
list.add(dto1);
list.add(dto2);
Assert.assertEquals("name1",mtDemo.testLambda(list).get(0).getName());
}
这时MT会强制修改lambda中的fiter部分,直接改为treu/false生成两个Mutant,会有一种没有覆盖到,MT会标注SURVIVED
//再增加一个UT,覆盖到name1不存在的情况
@Test
public void testLambda2(){
List list = new ArrayList<>();
MTDemo.TestDto dto2 = new MTDemo.TestDto("name2","type2");
list.add(dto2);
Assert.assertEquals(0,mtDemo.testLambda(list).size());
}