2019.6.26 补充与修正了短路相关的内容。
2019.9.16 修正了短路部分的错误,顺便去掉了用词不合适的前言
目前我所了解到的逻辑覆盖(而非路径覆盖)型的白盒测试大概有这几种:
- SC,语句覆盖
- DC,决策覆盖(也译作判定覆盖)
- CC,条件覆盖(也译作状态覆盖)
- C/DC,条件决策覆盖
- MC/DC,Modified C/DC,即修正的条件决策覆盖
- MCC,Multiple CC,即多重条件覆盖(也译作条件组合覆盖)
在此之前,困扰我最久的一个问题就是,什么是条件(Condition),什么是决策(Decision)?这个是搞清楚上述六种测试的前置条件(加上后置条件和不变式variant就是契约了,笑)。然后我看到了一个定义:
Condition is a Boolean expression containing no Boolean operators.
Decision is a Boolean expression composed of conditions and zero or more Boolean operators.
简单说来,条件是一个布尔表达式(比如a < b
),决策是几个条件用逻辑运算符拼在一起(比如a < b && c < d
)。一定要注意,多个表达式用与或非(当然了,还有异或)连接起来,构成的那个整体,是决策而不是条件。我一开始把整体当成了条件,造成了很大的困扰。
解决了这个定义问题,就来看看这六种逻辑覆盖测试。
其实我觉得第六种多重条件覆盖MCC,也就是所谓的枚举法,应该是最容易想到的一种方案。要实现逻辑覆盖,我把所有的情况列举一遍,搞个真值表,不就好了吗?
int count = 0;
if(a || b) {
count += 10;
}
if(c) {
count += 20;
}
// 测试1:
// in: a = true, b = true, c = true
// expect: count = 30
// 测试2:
// in: a = true, b = true, c = false
// expect: count = 10
// 测试3:
// in: a = true, b = false, c = true
// expect: count = 30
// 测试4:
// in: a = true, b = false, c = false
// expect: count = 10
// 测试5:
// in: a = false, b = true, c = true
// expect: count = 30
// 测试6:
// in: a = false, b = true, c = false
// expect: count = 10
// 测试7:
// in: a = false, b = false, c = true
// expect: count = 20
// 测试8:
// in: a = false, b = false, c = false
// expect: count = 0
话虽如此,当条件增加的时候,MCC的复杂度是呈指数上升的,而且考虑到可能存在的短路求值(在这里,只要a
为true
,就没必要考虑b
的取值了),有一些测试其实是无意义的。这不符合工程的理念(完全忽视了成本和效率)。于是,为了向现实妥协,就有了其他几种折衷的方案。
第一种,语句覆盖SC,顾名思义,就是覆盖每一条语句,让每一条语句都执行一次。当然了,只要都执行一次就行,比如a || b
这个表达式,能满足其中一个为真,让决策为真就可以了:
int count = 0;
if(a || b) {
count += 10;
}
if(c) {
count += 20;
}
// 测试1:
// in: a = true, b = false, c = true
// expect: count = 30
// 测试2:
// in: a = true, b = false, c = false
// expect: count = 10
在这里可以看到,只要a || b
为真,就能确保if
块里的语句被执行。从这个角度来说,语句覆盖是只考虑真不考虑假的(如果不包括else
,如果包括else
,为了覆盖肯定是需要考虑为假的情况的)。
这种覆盖率显然是不行的,表达式为假时有些情况就覆盖不到了,语句覆盖SC并没有考虑决策为假对结果的影响,比如上面的例子中,如果a || b
为假,count
就是0或者20,但我写测试用例的时候并不会考虑这一点,因为语句实际上都覆盖到了。在这里因为简单,所以不会有什么问题,但规模大了之后,这个决策可能会带来的副作用就会产生不可预知的问题,这种低覆盖率的弊端就会很明显。
为了解决这个问题,就有了第二种,决策覆盖DC。这也很符合人的认知过程,因为既然表达式为假时有些语句覆盖不到,那我把为假时的情况考虑进去不就行了吗?也就是说,需要同时考虑决策为真和为假时的情况。还用上面那个例子,也就是说,不仅需要考虑a || b
为真的情况,还要考虑a || b
为假的情况:
int count = 0;
if(a || b) {
count += 10;
}
if(c) {
count += 20;
}
// 测试1:
// in: a = true, b = false, c = true
// expect: count = 30
// 测试2:
// in: a = false, b = false, c = false
// expect: count = 0
有什么变化吗?似乎并没有。设想很美好,考虑了决策为假的情况,但测试覆盖率好像没什么太大的变化。究其原因,还是决策这个粒度太大了。面向代码的白盒测试应该是细粒度的,这里却有点类似于面向规格的黑盒测试的意味,把粗粒度的决策作为考察对象,不是很合适。所以,就有了第三种,条件覆盖CC。这次,我们把关注点放到了条件上,要覆盖每一个条件为真和为假的情况:
int count = 0;
if(a || b) {
count += 10;
}
if(c) {
count += 20;
}
// 测试1:
// in: a = true, b = true, c = true
// expect: count = 30
// 测试2:
// in: a = false, b = false, c = false
// expect: count = 0
……似乎还是没什么变化。因为这次虽然粒度细了,但考虑得很粗,因为有些条件是相互关联的,要进行协作,形成决策,才能看出他们真正的影响,割裂来看还是有点考虑不周。既然如此,那我既覆盖决策,又覆盖条件,这总行了吧?所以,我们有了第四种,条件决策覆盖C/DC:
int count = 0;
if(a || b) {
count += 10;
}
if(c) {
count += 20;
}
// 测试1:
// in: a = true, b = true, c = true
// expect: count = 30
// 测试2:
// in: a = false, b = false, c = false
// expect: count = 0
为什么还是不行?!这次的思路是对的,但是还是有所疏忽。不知道你发现没有,和开头所说的短路求值一样,虽然同时覆盖了决策和条件,但某些条件的改变并不会影响决策。我们需要考虑的是那些重要的条件,足以影响决策的条件。这不正是软件测试的目的吗?所以,我们有了最后一种,也就是修正的条件决策覆盖MC/DC。这一次,我们在考虑决策和条件的基础上,要求每一个条件都是“重要”的;也就是说,每一个条件的改变,都会影响所在决策的值:
int count = 0;
if(a || b) {
count += 10;
}
if(c) {
count += 20;
}
// 测试1:
// in: a = true, b = false, c = true
// expect: count = 30
// 测试2:
// in: a = false, b = true, c = false
// expect: count = 10
// 测试3:
// in: a = false, b = false, c = false
// expect: count = 0
值得一提的是,改变决策的值,不一定是改变整体的决策值,有可能只是改变它所在的决策的值。比如,对于a && (b || c)
的一个测试用例{ TFT }来说,看似第二个条件无关紧要,无论取T还是F都不改变整个决策的值,但它的改变会影响它所在的b || c
决策的值啊。无论如何,只要能改变决策,就很“重要”了。
可以看到,这里的覆盖率有了明显的提高,虽然和MCC比起来还是有所不足,但比起之前的任何一种都要好,而且只增加了一个用例,就提高了25%的覆盖率(2/4->3/4),简直就是成本和性能的完美平衡(笑)。
这里有一个相对专业的定义:
Modified Condition/Decision Coverage: every point of entry and exit in the program has been invoked at least once,every condition in the program has taken all possible outcomes at least once,and each condition in a decision has been shown to independently affect a decision.
而且,还有一个很有意思的细节:MC/DC有类似于短路的实现。对于a && (b || c)
这个决策来说,当a
为假时,后面的b || c
就不那么“重要”了(因为无法影响决策),所以b、c可以随意取值,这样就减少了测试用例的数目,降低了测试成本。当然,这一条可能只是对不包含短路机制的语言有意义,因为包含短路机制的语言会自行处理不“重要”的条件,不需要测试用例亲自出手。
但短路带来的是测试用例的覆盖问题。因为在触发短路后,后面的状态是没有覆盖到的。举个例子吧,这里我们用-
表示随便取什么值。对于a && ( b || c )
这个整体的决策来说,如果取测试用例为F–,事实上b、c都是没有覆盖到的。因此,为了实现CC(以及后续的C/DC和MC/DC),就需要增加测试用例。所以,这里无论是C/DC还是MC/DC,测试集都应当取为{ F–, TT-, TFT, TFF }。
事实上这里不够准确。可以用控制变量的方法,让某个条件成为足以影响决策的重要条件。可能比较合适的TR是{ FT-, TTF, TFT, TFF }。
事实上,可以利用短路机制将布尔表达式转换成类似于if-else的结构。比如,我们可以把a && b
转换成下面的结构:
if (a) {
if (b) {
// ...
}
}
结束。