前言
需要注意,单测的一个大前提就是需要清楚的知道自己要测试的程序块所预期的输入输出,然后根据这个预期和程序逻辑来书写case。
(这里需要注意的就是单测的预期结果?一定要针对需求/设计逻辑去写,而不是针对实现去写,否则单测将毫无意义,照着错误的实现设计出的case也很可能是错的)
覆盖类型
1、行覆盖?Statement Coverage
行覆盖(又叫语句覆盖)就是通过设计一定量的测试用例,保证被测试的方法每一行代码都会被执行一遍。
路径覆盖是最弱的覆盖方式。
实例:
public Integer fun3(Integer a, Integer b, Integer x) {
if (a > 1 && b == 0) {
x = x + a;
}
if (a == 2 || x > 1) {
x += 1;
}
return x;
}
本例仅需要一个case,即可实现行覆盖。test case 如下:
@Test
public void testFun3StatementCoverage(){
Integer res = demoService.fun3(2,0,3);
Assert.assertEquals(6,res.intValue());
}
这个用例就可以保证所有的行都被执行。
但是仅仅有这一个用例的话,对这个方法的测试就是非常脆弱的。
举个栗子,某RD接到了这个需求,理清了逻辑,写好单测之后开始写代码(或者写好代码之后开始写单测)。但是由于手抖,将第三行的 && 写成了 ||:
public Integer fun4(Integer a, Integer b, Integer x) {
if (a > 1 || b == 0) {
x += a;
}
if (a == 2 || x > 1) {
x += 1;
}
return x;
}
然后跑一下单测,发现很顺滑,一下就过了。
随后该RD很高兴的将代码发布到了线上,结果就发生了严重的生产故障,于是该RD就被开除了。
行覆盖是一个最基础的覆盖方式,但是也是最薄弱的,如果完全依赖行覆盖,那不小心就会被开除。
2、判定覆盖 / 分支覆盖 (Decision Coverage/Branch Coverage)
public Integer fun3(Integer a, Integer b, Integer x) {
if (a > 1 && b == 0) {
x = x + a;
}
if (a == 2 || x > 1) {
x += 1;
}
return x;
}
判定覆盖的含义就是代码里每一个判定都要走一次true,一次false。依然用上面的代码,想要实现判定覆盖,需要以下case
@Test
public void testFun3DecisionCoverage(){
Integer res = demoService.fun3(2,0,1);
Assert.assertEquals(4,res.intValue());
res = demoService.fun3(3,1,1);
Assert.assertEquals(1,res.intValue());
}
这两个用例可以保证判定 A: (a > 1 || b == 0)? 和判定B: (a == 2 || x > 1) 分别都取一次true 和false:
tc2 时, A,B均为true;tc3时,A,B均为false。
可以看出分支覆盖依然有明显缺陷,并没有覆盖到? A: true? B: false 和 A:false B:true的情况。
3、条件覆盖 Condition Coverage
public Integer fun3(Integer a, Integer b, Integer x) {
if (a > 1 && b == 0) {
x = x + a;
}
if (a == 2 || x > 1) {
x += 1;
}
return x;
}
条件覆盖和判定覆盖类似,不过判定覆盖着眼于整个判定语句,而条件覆盖则着眼于某个判断条件。
条件覆盖需要保证每个判断条件的true false都要覆盖到,而不是整个判定语句。
例如,判定A?(a > 1 || b == 0) ,只需要整个判定表达式分别取一次真假即可满足判定覆盖,而要满足条件覆盖,则需要判断条件 (a>1) 和 (b==0) 分别都取一次true false才算满足。
依然采用同样的代码,要想实现条件覆盖,则需要:
@Test
public void testFun3ConditionCoverage(){
Integer res = demoService.fun3(2,0,3);
Assert.assertEquals(6,res.intValue());
res = demoService.fun3(0,1,0);
Assert.assertEquals(0,res.intValue());
}
这两个用例可以保证?(a > 1)?? (b==0) (a == 2)?(x > 1) 四个条件都分别取true false。
很明显可以发现,这玩意儿依然是不全面的,这个例子里条件覆盖和判定覆盖存在同样的问题,覆盖的不够全面。
4、路径覆盖 Path Coverage
public Integer fun3(Integer a, Integer b, Integer x) {
if (a > 1 && b == 0) {
x = x + a;
}
if (a == 2 || x > 1) {
x += 1;
}
return x;
}
路径覆盖这个顾名思义就是覆盖所有可能执行的路径。
为了方便理解,这里先把流程图画出来。
红色代表一段路径。
首先梳理所有路径:
路径1:1-->3-->5;
路径2:1-->2-->5;
路径3:1-->3-->4;
路径4:1-->2-->4;
路径覆盖就是需要设计用例,将所有的路径都走一遍。
设计以下用例:
@Test
public void testFun3PathCoverage(){
Integer res = demoService.fun3(0,1,0);
Assert.assertEquals(0,res.intValue());
res = demoService.fun3(3,0,-3);
Assert.assertEquals(0,res.intValue());
res = demoService.fun3(2,1,3);
Assert.assertEquals(4,res.intValue());
res = demoService.fun3(2,0,3);
Assert.assertEquals(6,res.intValue());
}
总结
这是最常见的几种覆盖类型,行覆盖、判定覆盖、条件覆盖优缺点一致,都是较为简单,方便构造用例,并且能对程序质量有一定保证作用。
路径覆盖最完善,但是在一些复杂的场景里,会带来测试代码指数级增长的副作用,这个对于绝大多数人都是无法接受的。
一般情况下,行覆盖和判定覆盖同时做到已经是比较良好的单测代码了,jacoco的覆盖率统计也都是基于判定覆盖的。
本文内容不用于商业目的,如涉及知识产权问题,请权利人联系51Testing小编(021-64471599-8017),我们将立即处理