简介
测试构想是以故障模型为基础的,是关于软件中哪些故障看似可能以及如何最大程度地找到这些故障的想法。本指南显示如何根据布尔表达式和关系表达式来形成测试构想。它首先着眼于代码来促动技术,然后描述在尚未编写出代码或代码不可用的情况下如何应用这些技术。
布尔表达式
考虑从管理炸弹爆炸的(虚构)系统中提取的以下代码片段。它是安全系统的一部分,并且控制着是否遵照指令按下“引爆炸弹”按钮。
if (publicIsClear || technicianClear) {
bomb.detonate();
}
代码有错误。|| 应为 &&.
该错误将造成严重影响。炸弹不会在炸弹技术人员和人群均完全离开时才引爆,而是在两者之一完全离开时就引爆。
什么测试能找到这个错误?
考虑关于在技术人员和人群均完全离开后按下按钮的测试。代码将允许引爆炸弹。但是,正确的代码(使用 &&
的代码)会执行同样的操作(这点很重要)。因此,该测试对故障查找无效。
类似地,当技术人员和人群均在炸弹附近时,该错误代码将正确运作:炸弹不会引爆。
要找出错误,您的某一用例中的已编写代码的求值必须不同于应已编写的代码。例如,人群必须完全离开,但炸弹技术人员仍在炸弹附近。这里是所有表格形式的测试:
publicIsClear
|
technicianClear
|
编写的代码...
|
正确的代码将...
|
|
true
|
true
|
引爆
|
已引爆
|
测试无效(对该故障)
|
true
|
false
|
引爆
|
未引爆
|
有效的测试
|
false
|
true
|
引爆
|
未引爆
|
有效的测试
|
false
|
false
|
不引爆
|
未引爆
|
测试无效(对该故障)
|
中间的两个测试对查找该特定故障均有效。但要注意,由于两个测试中的任何一个即可找出故障,所以您无需运行两个测试,其中一个是多余的。
该表达式还可能存在其他错误。这里是布尔表达式中常见错误的两个列表。左边的故障都是由此处讨论的方法找到的。右边的故障则可能并非如此。因此,该方法无法如我们所希望的那样找到所有故障,但它仍是有效的。
检测到的故障
|
可能未检测到的故障
|
使用了错误的运算符:a || b 应为 a &&b
|
使用了错误的变量:a&&b&&c 应为 a&& x&&d
|
“非”运算被省略或不正确:a||b 应为 !a||b,或者 !
a||b 应为 a||b
|
表达式太简单:a&&b 应为 a&&b&&c
|
未对表达式配置正确的括号:a&&b||c 应为 a&&(b||c)
|
含有左列中一个以上故障的表达式
|
表达式过于复杂:a&&b&&c 应为 a&&b
(该故障不大可能出现,但是使用针对其他理由有效的测试可轻易找出该故障。)
|
|
如何使用这些观点?假设给您一个布尔表达式,比如 a&&!b。则您可以构造这样一个真值表:
a
|
b
|
a&&!b
(已编写的代码)
|
可能应为
a||!b
|
可能应为
!a&&!b
|
可能应为
a&&b
|
...
|
true
|
true
|
false
|
true
|
false
|
true
|
...
|
true
|
false
|
true
|
true
|
false
|
false
|
...
|
false
|
true
|
false
|
false
|
false
|
false
|
...
|
false
|
false
|
false
|
true
|
true
|
false
|
...
|
如果您查看过所有可能的表达式,就会发现第一种、第二种和第四种可能的表达式就是所需要的表达式。第三种表达式将找不出其他表达式也找不出的错误,因此您无需尝试。(表达式越复杂,不需要的用例所带来的节约就增长越快。)
当然,有头脑的人是不会构画这种表格的。幸运的是,您无需如此。简单表达式所需的用例是很好记的。(请参阅下一节。)有关更复杂的表达式,例如 A&&B||C,请参阅“与”和“或”混合的测试构想,其中列出了关于具有两个或三个运算符的表达式的测试构想。关于更为复杂的表达式,可使用程序来生成测试构想。
简单布尔表达式表
如果表达式为 A&&B,则使用以下值进行测试:
A
|
B
|
true
|
true
|
true
|
false
|
false
|
true
|
如果表达式为 A||B,则使用以下值进行测试:
A
|
B
|
true
|
false
|
false
|
true
|
false
|
false
|
如果表达式为 A1 && A2 && ... &&
An,则使用以下值进行测试:
A1,A2、...,以及 An 均为 true
|
A1 为 false,其余均为 true
|
A2 为 false,其余均为 true
|
...
|
An 为 false,其余均为 true
|
如果表达式为 A1 || A2 || ... || An,则使用以下值进行测试:
A1、A2、...,以及 An 均为 false
|
A1 为 true,其余均为 false
|
A2 为 true,其余均为 false
|
...
|
An 为 true,其余均为 false
|
如果表达式为 A,则使用以下值进行测试:
因此,当需要测试 a&&!b 时,可应用上述第一个表格,反转 b 的含义(因为 b 被求反),则得到这样一列测试构想:
-
A true,B false
-
A true,B true
-
A false,B false
关系表达式
这是另外一个含有故障的代码示例:
if (finished < required) {
siren.sound();
}
< 应为 <=。这样的错误相当常见。使用布尔表达式,您可以构造测试值的表格并查看哪些测试会检测到该故障:
finished
|
required
|
编写的代码...
|
正确的代码将...
|
1
|
5
|
发出警报声
|
已发出警报声
|
5
|
5
|
不发出警报声
|
已发出警报声
|
5
|
1
|
不发出警报声
|
未发出警报声
|
更通常的是,只要 finished=required,就可以检测出故障。根据对似乎可能的故障进行的分析,我们可以得出测试构想的这些规则:
如果表达式为 A<B 或 A>=B,则使用以下值进行测试:
如果表达式为 A>B 或 A<=B,则使用以下值进行测试:
“略”表示什么意思?如果 A 和 B 是整数,则 A 应比 B 小 1 或是大 1。如果它们是浮点数,则 A 与 B 应非常接近。(它可能不一定是最接近 B 的浮点数。)
布尔和关系组合表达式的规则
大多数关系运算符是在布尔表达式中出现的,如此例中所示:
if (finished < required) {
siren.sound();
}
关系表达式的规则将引出这些测试构想:
-
finished 等于 required
-
finished 略小于此表达式:required
布尔表达式的规则将引出这些测试构想:
-
finished < required 应为 true
-
finished < required 应为 false
它可能是此表达式:finished 略小于此表达式:required, finished <
required 为 true,因此后者毫无意义。
如果此表达式为 false,则它毫无意义:finished = required, ,finished <
required。
如果关系表达式不包含布尔运算符(&& 和 ||),则忽略该表达式也是布尔表达式这一事实。
对于布尔运算符和关系运算符的组合而言,情况略为复杂,如下所示:
if (count<5 || always) {
siren.sound();
}
根据关系表达式,可得出:
根据布尔表达式,可得出:
-
count<5 true,always false
-
count<5 false,always true
-
count<5 false,always false
可将这些观点组合为三个更具体的测试构想。(在此要注意 count 为整数。)
-
count=4,always false
-
count=5,always true
-
count=5,always false
请注意 count=5 使用了两次。似乎仅使用一次更好(以允许使用其他值),但为什么终究使用了两次 5 来测试该表达式呢:count ? 使用 5
来测试一次,使用其他值来测试另一次,这样结果就为 false,难道不是更好吗?(示例:count<5 这样会更好,但这样尝试是非常危险的。因为这样做很容易出错。假设进行如下尝试:
-
count=4,always false
-
count=5,always true
-
count<5 false,always false
假设存在某种故障,而该故障只有在使用以下值时才能够被捕获:count=5。这意味着在表达式 count<5 中,使用值 5
会得出“false”,而正确代码应得出 true。然而,该 false 值随即被替换为值 always,而该值为 true。这就是说,即使关系子表达式的值是错误的,整个表达式的值也是正确的。这将无法发现该故障。
如果其他 count=5 不那么具体,就会发现该故障。
当关系表达式位于布尔运算符的右侧时,会发生类似的问题。
由于难以知道哪些子表达式必须精确以及哪些可以笼统,所以最好使所有表达式都精确。备选方案是使用上述的布尔表达式程序。它会针对任意混合的布尔和关系表达式生成正确的测试构想。
无代码的测试构想
如概念:测试优先设计中所述,在实施代码之前先设计测试通常更为可取。因此,尽管技术是由代码示例启发的,但它们通常可在无代码的情况下应用。如何应用呢?
某些设计工件,例如状态表图和时序图,使用布尔表达式作为保护。这些用例是相当简明的,它们直接将来自布尔表达式的测试构想添加到工件的测试构想核对表中。请参阅工作产品指南:关于状态表图和活动图的测试构想。
当布尔表达式暗示而不明示时,情况就比较棘手了。描述 API 时常常是这种情况。这里是一个示例。考虑这一方法:
List matchList(Directory d1, Directory d1,
FilenameFilter excluder);
对该方法的行为的描述可能是这样的:
返回在两个目录中都出现的所有文件的绝对路径名的列表。子目录得以继承下来。返回列表中排除了与 excluder 相匹配的 [...] 文件名。excluder
仅适用于顶级目录,而不适用于子目录中的文件名。
没有出现词语“与”和“或”。但如果返回列表中包含文件名呢?当它出现在第一个目录中且出现在第二个目录中,且它处于较低级目录中或未被特别排除。在以下代码中:
if (appearsInFirst && appearsInSecond &&
(inLowerLevel || !excluded)) {
add to list
}
这里是该表达式的表格形式的测试构想:
appearsInFirst
|
appearsInSecond
|
inLower
|
excluded
|
true
|
true
|
false
|
true
|
true
|
true
|
false
|
false
|
true
|
true
|
true
|
true
|
true
|
false
|
false
|
false
|
false
|
true
|
false
|
false
|
从文本中发现隐含布尔表达式的一般方法是,首先列出所描述的操作(例如“返回匹配的名称”)。然后编写布尔表达式,描述采取某一操作的情况。从所有表达式中得出测试构想。
该流程中允许存在异议情况。例如,某人可能会写出上面使用的布尔表达式。另一人可能会认为存在两个截然不同的操作:首先,程序发现匹配的名称,然后将其过滤掉。因此,将有两个操作,而不是一个:
-
发现匹配项:
-
当某一文件在第一个目录中且在第二个目录中有同名文件时
-
过滤匹配项:
-
当匹配的文件在顶级目录中,且名称与 excluder 相匹配时
这些不同的方法会引出不同的测试构想以及不同的测试。但差异很可能并不特别重要。即,花时间考虑哪个表达式正确和尝试备用方法,还不如把时间投入到其他技术并生成更多的测试上。如果您很想知道可能的各种差异,请继续阅读下文。
第二个人会得出两组测试构想。
关于发现匹配项的测试构想:
-
文件在第一个目录中,文件在第二个目录中(true,true)
-
文件在第一个目录中,文件不在第二个目录中(true,false)
-
文件不在第一个目录中,文件在第二个目录中(false,true)
关于过滤匹配项的测试构想(一旦发现匹配项):
-
匹配的文件在顶级目录中,名称与 excluder 相匹配(true,true)
-
匹配的文件在顶级目录中,名称与 excluder 不匹配(true,false)
-
匹配的文件在某个较低级别的目录中,名称与 excluder 相匹配(false,true)
假设将这两组测试构想组合起来。第二组中的观点仅当文件在两个目录中都存在时起作用,因此仅可将这些观点与第一组中的第一个观点组合起来。得出以下结论:
文件在第一个目录中
|
文件在第二个目录中
|
顶级目录
|
匹配 excluder
|
true
|
true
|
true
|
true
|
true
|
true
|
true
|
false
|
true
|
true
|
false
|
true
|
关于发现匹配项的两个测试构想不出现在该表格中。我们可这样添加它们:
文件在第一个目录中
|
文件在第二个目录中
|
顶级目录
|
匹配 excluder
|
true
|
true
|
true
|
true
|
true
|
true
|
true
|
false
|
true
|
true
|
false
|
true
|
true
|
false
|
-
|
-
|
false
|
true
|
-
|
-
|
单元格为空白则表示该列无关。
该表现在看上去与第一个人得出的表非常相似。可使用相同的术语来强调该相似性。第一个人的表中有一列名为“inLower”,第二个人的表中有一列名为“in top
level”。通过交换值的意义,可转换这两列。这样,我们就得出第二个表的这一版本:
appearsInFirst
|
appearsInSecond
|
inLower
|
excluded
|
true
|
true
|
false
|
true
|
true
|
true
|
false
|
false
|
true
|
true
|
true
|
true
|
true
|
false
|
-
|
-
|
false
|
true
|
-
|
-
|
前三行与第一个人的表完全相同。后两行的区别在于该版本未指定值,而第一个人的表指定了值。这就得出了关于代码编写方式的假设。第一个人假设了一个复杂的布尔表达式:
if (appearsInFirst && appearsInSecond &&
(inLowerLevel || !excluded)) {
add to list
}
第二个人假设一个嵌套的布尔表达式:
if (appearsInFirst && appearsInSecond) {
// found match.
if (inTopLevel && excluded) {
// filter it
}
}
两者的区别在于:第一个人的测试构想将检测出第二个人的测试构想检测不出的两个故障,因为那些故障不适用。
-
在第一个实施中,可能存在括号引起的故障。|| 两边的括号正确吗?由于第二个实施中不含括号和 ||,因此不存在故障。
-
第一个实施的测试需求将进行检查以确定第二个表达式 && 是否应为 ||。在第二个实施中,显式的 && 被替换为隐式的 &&。其中没有 ||-for-&& 故障。(可能存在嵌套错误的情况,但该方法并不是针对这一点。)
|