TheArtofSoftwareTesting,错误代码118怎么回事

怎么回事 0

TheArtofSoftwareTesting SecondEdition GlenfordJ.Myers RevisedandUpdatedby TomBadgettandToddM.ThomaswithCoreySandler Copyright©2004byWordAssociation,Inc.Allrightsreserved.PublishedbyJohnWiley&Sons,Inc.,Hoboken,NewJersey.PublishedsimultaneouslyinCanada. 整理:丁兆杰,黄墀晖,黄米青,林嘉,马晓靖,王博,吴航,余华兵,俞培源v.1.27/1/2008 前言 1979年GlenfordJ.Myers出版了一本现在仍被证明为经典的著作,这就是本书的第1版。
本书经受住了时间的考验,25年来一直被列在出版商可供书目的清单中。
这个事实本身就是对本书稳定、基础和珍贵品质的佐证。
在同一时期,本书第2版的几位合著者共出版了120余本著作,大多数都是关于计算机软件的。
其中有一些很畅销,再版了多次(例如CoreySandler的《FixYourOwnPC》自付印以来已出版到第7版,TomBadgett关于微软PowerPoint及其他Office组件的著作已经出版到第4版以上)。
然而,这些作者的著作中没有哪一本书能够像本书一样持续数年之后仍畅销不衰。
区别究竟在哪里呢?这些新书只涵盖了短期性的主题:操作系统、应用软件、安全性、通信技术及硬件配置。
20世纪80年代和90年代以来的计算机硬件与软件技术的飞速发展,必然使得这些主题频繁地变功和更新。
在此期间出版的有关软件测试的书籍已数以十计、甚至数以百计。
这些书也对软件测试的主题进行了简要的探讨。
然而,本书为计算机界一个最为重要的主题提供了长期、基本的指南:如何确保所开发的所有软件做了其应该做的,并且同样重要的是,未做其不应该做的? 本书第2版中保留了同样的基本思想。
我们更新了其中的例子以包含更为现代的编程语言。
我们还研究了在Myers编著本书第1版时尚无人了解的主题:Web编程、电子商务及极限编程与测试。
但是,我们不会忘记,新的版本必须遵从其原著。
因此,新版本依然向读者展示GlenfordMyers全部的软件测试思想:这个思想体系以及过程将适用于当今乃至未来的软件和硬件平台。
我们也希望本书能够顺应时代,适用于当今的软件设计人员和开发人员掌握最新的软件测试思想及技术。
未经同意,严禁以任何形式拷贝 ii 引言 在本书1979年第1版出版的时侯,有一条著名的经验,即在一个典型的编程项目中,软件测试或系统测试大约占用50%的项目时间和超过50%的总成本。
25年后的今天,同样的经验仍然成立。
现在出现了新的开发系统、具有内置工具的语言以及习惯于快速开发大量软件的程序员。
但是,在任何软件开发项目中,测试依然扮演着重要角色。
在这些事实面前,读者可能会以为软件测试发展到现在不断完善,已经成为一门精确的学科。
然而实际情况并非如此。
事实上,与软件开发的任何其他方面相比,人们对软件测试仍然知之甚少。
而且,软件测试并非热门课题,本书首次出版时是这样.遗憾的是,今天仍然如此。
现在有很多关于软件测试的书籍和论文,这意味着,至少与本书首次出版时相比,人们对软件测试这个主题有了更多的了解。
但是,测试依然是软件开发中的“黑色艺术”。
这就有了更充足的理由来修订这本关于软件测试艺术的书,同时我们还有其他一些动机。
在不同的时期,我们都听到一些教授和助教说;”我们的学生毕业后进入了计算机界,却丝毫不了解软件测试的基本知识,而且在课堂上向学生介绍如何测试或调试其程序时,我们也很少有建议可提供。
” 因此,本书再版的目的与1979年时一样:填充专业程序员和计算机科学学生的知识空缺。
正如书名所蕴涵的,本书是对测试主题的实践探讨,而不是理论研究,连同了对新的语言和过程的探讨。
尽管可以根据理论的脉络来讨论软件测试,但本书旨在成为实用且“脚踏实地”的手册。
因此很多与软件测试有关的主题,如程序正确性的数学证明都被有意地排除在外了。
本书第l章介绍了一个供自我评价的测试,每位读者在继续阅读之前都须进行测试。
它揭示出我们必须了解的有关软件测试的最为重要的实用信息,即一系列心理和经济学问题,这些问题在第2章中进行了详细讨论。
第3章探讨的是不依赖计算机的代码走查或代码检查的重要概念。
不同于大多数研究都将注意力集中在概念的过程和管理方面,第3章则是从技术上“如何发现错误”的集度来进行探讨。
聪明的读者都会意识到,在软件测试人员的技巧中最为重要的部分是掌握如何编写有 iii 效测试用例的知识.这正是第4章的主题。
本书第5章和第6章分别探讨了如何测试单个模块或子程序及测试更夫的对象,而第7章则介绍了一些程序调试的实用建议,第8章讨论了极限编程和极限测试的概念,第9章介绍了如何将本书其他章节中详细讨论的软件测试的知识运用到web编程,包括电子商务系统中去。
本书面向三类主要的读者。
尽管我们希望本书中的内容对于专业程序员而言不完全是新的知识,但它应增强专业人员对测试技术的了解。
如果这些材料能使软件人员在某个程序中多发现一个错误,那么本书创造的价值将远远超过书价本身。
第二类读者是项目经理,因为本书中包含了测试过程管理的最新的、实用的知识。
第三类读者是计算机科学的学生,我们的目的在于向学生们展示程序测试的问题,并提供一系列有效的技术。
我们建议将本书作为程序设计课程的补充教材,让学生在学习阶段的早期就接触到软件测试的内容。
GlenfordJ.MyersTomBadgett ToddM.ThomasCoreySandler 未经同意,严禁以任何形式拷贝 iv 目录 第1章一个自我评价测试.......................................................................................1第2章软件测试的心理学和经济学..............................................................................4 2.1软件测试的心理学.............................................................................................42.2软件测试的经济学.............................................................................................72.3软件测试的原则...............................................................................................112.4小结...................................................................................................................15第3章代码检查、走查与评审....................................................................................163.1检查与走查(InspectionsAndWalkthroughs)...................................................173.2代码检查(CodeInspections).............................................................................183.3用于代码检查的错误列表...............................................................................203.4代码走查(Walkthroughs)..................................................................................293.5桌面检查(DeskChecking)................................................................................303.6同行评分(PeerRatings)....................................................................................313.7小结...................................................................................................................32第4章测试用例的设计................................................................................................334.1白盒测试(White-BoxTesting)..........................................................................344.2错误猜测(ErrorGuessing)...............................................................................684.3测试策略...........................................................................................................70第5章模块(单元)测试............................................................................................715.1测试用例设计...................................................................................................715.2增量测试...........................................................................................................805.3自顶向下测试与自底向上测试.......................................................................845.4执行测试...........................................................................................................91第6章更高级别的测试................................................................................................936.1功能测试(FunctionTesting)..............................................................................986.2系统测试(SystemTesting)................................................................................996.3验收测试(eptanceTesting).......................................................................1096.4安装测试(InstallationTesting)........................................................................109 6.5测试的计划与控制.........................................................................................1106.6测试结束准则.................................................................................................1126.7独立的测试机构.............................................................................................117第7章调试(DEBUGGING)........................................................................................1187.1暴力法调试(DebuggingbyBruteForce)........................................................1197.2归纳法调试(DebuggingbyInduction)...........................................................1207.3演绎法调试(DebuggingbyDeduction)..........................................................1237.4回溯法调试(DebuggingbyBacktracking).....................................................1267.5测试法调试(DebuggingbyTesting)...............................................................1267.6调试的原则.....................................................................................................127第8章极限测试..........................................................................................................1318.1极限编程基础.................................................................................................1318.2极限测试:概念.............................................................................................1358.3极限测试的应用.............................................................................................1378.4小结.................................................................................................................141词汇表............................................................................................................................142 未经同意,严禁以任何形式拷贝
6 软件测试的艺术 第1章一个自我评价测试 自本书25年前首次出版以来,软件测试变得比以前容易得多,也困难得多。
软件测试何以变得更困难?原因在于大量编程语言、操作系统以及硬件平台的出现。
在20世纪70年代只有相当少的人使用计算机,而今天在商业界和教育界,如果不使用计算机,几乎没有人能完成日常工作。
况且,计算机本身的功能也比以前增强了数百倍。
因此,我们现在编写的软件会潜在地影响到数以百万计的人,使他们更高效地完成工作,反之也会给他们带来数不清的麻烦,导致工作或事业的损失。
这并不是说今天的软件比本书第一版发行时更重要,但可以肯定地说,今天的计算机——以及驱动它的软件——无疑已影响到了更多的人、更多的行业。
就某些方面而言,软件测试变得更容易了,因为大量的软件和操作系统比以往更加复杂,内部提供了很多已充分测试过的例程供应用程序集成,无须程序员从头进行设计。
例如,图形用户界面(GUI)可以从开发语言的类库中建立起来,同时,由于它们是经过充分调试和测试的可编程对象,将其作为用户应用程序的组成部分进行测试的要求就减少了许多。
所谓软件测试,就是一个过程或一系列过程.用来确认计算机代码完成了其应该完成的功能不执行其不该有的操作。
软件应当是可预测且稳定的,不会给用户带来意外惊奇。
在本书中,我们将讨论多种方法来达到这个目标。
好了,在开始阅读本书之前,我们想让读者做一个小测验。
我们要求设计一组测试用例(特定的数据集合),适当地测试一个相当简单的程序。
为此要为该程序建立一组测试数据,程序须对数据进行正确处理以证明自身的成功。
下面是对程序的描述: 这个程序从一个输入对话框中读取三个整数值。
这三个整数值代表了三角形三边的长度。
程序显示提示信息,指出该三角形究竟是不规则三角 未经同意,严禁以任何形式拷贝
1 第1章一个自我评价测试 形、等腰三角形还是等边三角形。
注意,所谓不规则三角形是指三角形中任意两条边不相等,等腰三角形是指有两条边相等,而等边三角形则是指三条边相等。
另外,等腰三角形等边的对角也相等(即任意三角形等边的对角也相等),等边三角形的所有内角都相等。
用你的测试用例集回答下列问题,借以对其进行评价。
对每个回答“是”的答 案,可以得l分:
1.是否有这样的测试用例,代表了二个有效的不规则三角形?(注意,如
1, 2,3和2,5,10这样的测试用例并不能确保“是”的答案,因为具备这 样边长的三角形不存在。

2.是否有这样的测试用例,代表一个有效的等边三角形?
3.是否有这样的测试用例,代表一个有效的等腰三角形?(注意如2,2,
4 的测试用例无效,因为这不是一个有效的三角形。

4.是否能少有三个这样的测试用例,代表有效的等腰三角形,从而可以测试 到两等边的所有三种可能情况?(如3,3,4;3,4,3;4,3,3)
5.是否有这样的测试用例,某边的长度等于0?
6.是否有这样的测试用例,某边的长度为负数?
7.是否有这样的测试用例,三个整数皆大于
0,其中两个整数之和等于第
个?(也就是说,如果程序判断l,2,3表示一个不规则二角形,它可能 就包含一个缺陷。

8.是否至少有三个第7类的测试用例,列举了一边等于另外两边之和的全部 可能情况(如1,2,3;1,3,2;3,1,2)?
9.是否有这样的测试用例,三个整数皆大于
0,其中两个整数之和小于第
个整数?(如1,2,4;12,15,30) 10.是否至少有三个第9类的测试用例,列举了一边大于另外两边之和的全 部可能情况?(如1,2,4;1,4,2;4,1,2) 11.是否有这样的测试用例,三边长度皆为0(0,0,0)? 12.是否至少有一个这样的测试用例,输入的边长为非整数值(如2.5,3.5, 5.5) 13.是否至少有一个这样的测试用例,输入的边长个数不对(如仅输入了两 未经同意,严禁以任何形式拷贝
2 软件测试的艺术 个而不是三个整数)?
14.对于每一个测试用例,除了定义输入值之外,是否定义了程序针对该输 入值的预期输出值? 当然,测试用例集即使满足了上述条件,也不能确保能查找出所有可能的错误。
但是,由于问题1至问题13代表了该程序不同版本中已经实际出现的错误,对该程序进行的充分测试至少应该能够暴露这些错误。
开始关注自己的得分之前,请考虑以下情况:以我们的经验来看,高水平的专业程序员平均得分仅7.8(满分14)。
如果读者的得分更高,那么祝贺你。
如果没有那么高,我们将尽力帮助你。
这个测验说明,即使测试这样一个小的程序,也不是件容易的事。
如果确实是这样,那么想象一下测试一个十万行代码的空中交通管制系统、一个编译器,甚至一个普通的工资管理程序的难度。
随着面向对象编程语言(如Java、C++)的出现,测试也变得更加困难。
举例来说,为测试这些语言开发出来的应用程序,测试用例必须要找出与对象实例或内存管理有关的错误。
从上面这个例子来看,完全地测试一个复杂的、实际运行的程序似乎是不太可能的。
情况并非如此!尽管充分测试的难度令人望而生畏,但这是软件开发中一项非常必需的任务,也是可以实现的一部分工作,通过本书我们可以认识到这一点。
未经同意,严禁以任何形式拷贝
3 第2章软件测试的心理学和经济学 第2章软件测试的心理学和经济学 软件测试是一项技术性工作,但同时也涉及经济学和人类心理学的一些重要因素。
在理想情况下,我们会测试程序的所有可能执行情况。
然而,在大多数情况下,这几乎是不可能的,即使一个看起来非常简单的程序,其可能的输入与输出组合可达到数百种甚至数千种,对所有的可能情况都设计测试用例是不切合实际的。
对一个复杂的应用程序进行完全的测试,将耗费大最的时间和人力资源,以至于在经济上是不可行的。
另外,要成功地测试一个软件应用程序,测试人员也需要有正确的态度(也许用“愿景(vision)”这个词会更好一些)。
在某些情况下,测试人员的态度可能比实际的测试过程本身还要重要。
因此,在深入探讨软件测试更加技术化的本质之前,我们先探讨一下软件测试的心理学和经济学问题。
2.1软件测试的心理学 测试执行得差,其中一个主要原因在于大多数的程序员一开始就把“测试”这个术语的定义搞错了,他们可能会认为: •“软件测试就是证明软件不存在错误的过程。
”•“软件测试的目的在于证明软件能够正确完成其预定的功能。
”•“软件测试就是建立一个‘软件做了其应该做的’信心的过程。
” 这些定义都是本末倒置的。
每当测试一个程序时,总是想为程序增加一些价值。
通过测试来增加程序的价值,是指测试提高了程序的可靠性或质量。
提高了程序的可靠性,是指找出并最终修改了程序的错误。
因此不要只是为了证明程序能够正确运行而去测试程序;相反,应该一开始就假设程序中隐藏着错误(这种假设对于几乎所有的程序都成立),然后测试程序,发现尽可能多的错误。
那么,对于测试,更为合适的定义应该是: 未经同意,严禁以任何形式拷贝
4 软件测试的艺术 “测试是为发现错误而执行程序的过程”。
虽然这看起来像是个微妙的文字游戏,但确实有重要的区别。
理解软件测试的真正定义,会对成功地进行软件测试有很大的影响。
人类行为总是倾向于具有高度目标性,确立一个正确的目标有着重要的心理学影响。
如果我们的目的是证明程序中不存在错误,那就会在潜意识中倾向于实现这个目标,也就是说,我们会倾向于选择可能较少导致程序失效的测试数据。
另一方面,如果我们的目标在于证明程序中存在错误,我们设计的测试数据就有可能更多地发现间题。
与前一种方法相比,后一种方法会更多地增加程序的价值。
这种对软件测试的定义,包含着无穷的内蕴,其中的很多都蕴涵在本书各处。
举例来说,它暗示了软件测试是一个破坏性的过程,甚至是一个“施虐”的过程,达就说明为什么人多数人都觉得它困难。
这种定义可能是违反我们愿望的,所幸的是,我们大多数人总是对生活充满建设性而不是破坏性的愿景。
大多数人都本能地倾向于创造事物,而不是将事物破坏。
这个定义还暗示了对于一个特定的程序:应该如何设计测试用例(测试数据)、哪些人应该而哪些人又不应该执行测试。
为增进对软件测试正确定义的理解,另一条途径是分析一下对“成功的”和“不成功的”这两个词的使用,当项目经理在归纳测试用例的结果时,尤其会用到这两个词。
大多数的项日经理将没发现错误的测试用例称为一次“成功的测试”,而将发现了某个新错误的测试称为“不成功的测试”。
这又是一次本末倒置。
“不成功的”表示事情不遂人意或令人失望。
我们认为,如果在测试某段程序时发现了错误,而且这些错误是可以修复的,就将这次合理设计并得到有效执行的测试称作是“成功的”。
如果本次测试可以最终确定再无其他可查出的错误,同样也被称作是“成功的”。
所谓“不成功的”测试,仅指未能适当地对程序进行检查,在大多数情况下,未能找出错误的测试被认为是“不成功的”,这是因为认为软件中不包含错误的观点基本上是不切实际的。
能发现新错误的测试用例不太可能被认为是“不成功的”;相反,能发现错误就证明它是值得设计的。
一个“不成功的”测试用例.会使程序输出正确的结果,但不能发现任何错误。
未经同意,严禁以任何形式拷贝
5 第2章软件测试的心理学和经济学 我们可以类比一下病人看医生的情况,病人因为身体不舒服而去看医生。
如果医生对病人进行了某些实验检测,却没有诊断出任何病因,我们就不会认为这此实验检测是“成功的”。
之所以是“不成功的”检侧,是因为病人支付了昂贵的实验检测费用,而病状却依然如故。
病人会因此而质疑医生的诊断能力。
但是,如果实验检测诊断出病人是胃溃疡,那么这次检测就是“成功的”,医生可以开始进行适当的治疗。
因此.医疗行业会使用“成功的”或“不成功的”来表达适当的意思。
我们当然可以类推到软件测试中来,当我们开始测试某个程序时,它就好似我们的病人。
“软件测试就是证明软件不存在错误的过程”,这个定义会带来第二个问题。
对于几乎所有的程序而言,甚至是非常小的程序,这个目标实际上也是无法达到的。
另外,心理学研究表明,当人们开始一项工作时,如果已经知道它是不可行的或无法实现时,人的表现就会相当糟糕。
举例来说,如果要求人们在15分钟之内完成星期日《纽约时报》里的纵横填字游戏,那么我们会观察到l0分钟之后的进展非常小,因为大多数人都会却步于这个现实,即这个任务似乎是不可能完成的。
但是如果要求在四个小时之内完成填字游戏,我们很可能有理由期望在最初10分钟之内的进展会比前一种情况下的大。
将软件测试定义为发现程序错误的过程,使得测试是个可以完成的任务,从而克服了这个心理障碍。
诸如“软件测试就是证明‘软件做了其应该做的’的过程”此类的定义所带来的第三个问题是,程序即使能够完成预定的功能,也仍然可能隐藏错误。
也就是说,当程序没有实现预期功能时,错误是清晰地显现出来的;如果程序做了其不应该做的,这同样是一个错误;考虑一下第l章中的三角形测试程序。
即使我们证明了程序能够正确识别出不规则三角形、等腰三角形和等边三角形,但是在完成了不应执行的任务后(例如将1,2,3说成是一个不规则三角形或将0,0,0说成是一个等边三角形),程序仍然是错的。
如果我们将软件测试视作发现错误的过程,而不是将其视为证明“软件做了其应该做的”的过程,我们发现后一类错误的可能性会大很多。
总结一下,软件测试更适合被视为试图发现程序中错误(假设其存在)的破坏性的过程。
一个成功的测试用例,通过诱发程序发生错误,可以在这个方向上促进 未经同意,严禁以任何形式拷贝
6 软件测试的艺术 软件质量的改进。
当然,最终我们还是要通过软件测试来建立某种程度的信心:软件做了其应该做的,未做其不应该做的。
但是通过对错误的不断研究是实现这个目的的最佳途径。
有人可能会声称“本人的程序完美无缺”(不存在错误),针对这种情况建立起信心的最好办法就是尽量反驳他,即努力发现不完美之处,而不只是确认程序在某些输入情况下能够正确地工作。
2.2软件测试的经济学 给出了软件测试的适当定义之后,下一步就是确定软件测试是否能够发现“所有”的错误。
我们将证明答案是否定的,即使是规模很小的程序。
一般说来,要发现程序中的所有错误也是不切实际的,常常也是不可能的。
这个基本的问题反过来暗示出软件测试的经济学问题、测试人员对被测软件的期望,以及测试用例的设计方式。
为了应对测试经济学的挑战,应该在开始测试之前建立某些策略。
黑盒测试和白盒测试是两种最普遍的策略,我们将在下面两节中讨论。
2.2.1黑盒测试 黑盒测试是一种重要的测试策略,又称为数据驱动的测试或输入/输出驱动的测试。
使用这种测试方法时,将程序视为一个黑盒子。
测试目标与程序的内部机制和结构完全无关,而是将重点集中放在发现程序不按其规范正确运行的环境条件。
在这种方法中,测试数据完全来源于软件规范(换句话说,不需要去了解程序的内部结构)。
如果想用这种方法来发现程序的所有错误,判定的标准就是“穷举输入测试”,将所有可能的输入条件都作为测试用例。
为什么这样做?比如说在三角形测试的程序中,试过了三个等边三角形的测试用例。
这不能确保正确地判断出所有的等边三角形。
程序中可能包含对边长为3842,3842,3842的特殊检查,并指出此三角形为不规则三角形。
由于程序是个黑盒子,因此能够确定此条语句存在的惟一方法,就是试验所有的输入情况。
要穷举测试这个三角形程序,可能需要为所有有效的三角形创建测试用例,只 未经同意,严禁以任何形式拷贝
7 第2章软件测试的心理学和经济学 要三角形边长在开发语言允许的最大整数值范围内。
这些测试用例本身就是天文数字,但这还决不是所谓穷尽的,当程序指出-3,4,5是一个不规则三角形或
2,A,2是一个等腰三角形时,问题就暴露出来了,为了确保能够发现所有这样的错误,不仅得用所有有效的输入,而且还得用所有可能的输入进行测试。
因此,为了穷举测试三角形程序,实际上需要创建无限的测试用例:这当然是不可能的。
如果测试这个三角形程序都这么难的话,那么要穷举测试一个稍大些的程序的难度就更大了,设想一下,如果要对一个C++编译器进行黑盒穷举测试,不仅要创建代表所有有效C++程序的测试用例(实际上,这又是个无穷数),还需要创建代表所有无效C++程序的测试用例(无穷数),以确保编译器能够检测出它们是无效的。
也就是说,编译器必须进行测试,确保其不会执行不应执行的操作——如顺利地编译成功一个语法上不正确的程序。
如果程序使用到数据存储,如操作系统或数据库应用程序,这个问题会变得尤为严重。
举例来说,在航班预定系统这样的数据库应用程序中,诸如数据库查询、航班预约这样的事务处理需要随上次事务的执行情况而定,因此,不仅要测试所有有效的和无效的事务处理,还要测试所有可能的事务处理顺序。
上述讨论说明,穷举输入测试是无法实现的,这有两方面的含义,一是我们无法测试一个程序以确保它是无错的,二是软件测试中需要考虑的一个基本问题是软件测试的经济学。
也就是说,由于穷举测试是不可能的,测试投人的目标在于通过有限的测试用例,最大限度地提高发现的问题的数量,以取得最好的测试效果。
除了其他因素之外,要实现这个目标,还需要能够窥见软件的内部,对程序作些合理但非无懈可击的假设(例如,如果三角形程序将2,2,2视为等边三角形,那就有理由认为程序对3,3,3也作同样判断)。
这种思路将形成本书第4章中测试用例设计策略的部分方法。
2.2.2白盒测试 另一种测试策略称为白盒测试或称逻辑驱动的测试,允许我们检查程序的内部结构。
这种测试策略对程序的逻辑结构迸行检查,从中获取测试数据(遗憾的是,常常忽略了程序的规范)。
未经同意,严禁以任何形式拷贝
8 软件测试的艺术 在这里我们的目标是针对达种测试策略,建立起与黑盒测试中穷举输入测试相似的测试方法。
也许有一个解决的办法,即将程序中的每条语句至少执行一次。
但是我们不难证明,这还是远远不够的。
这种方法通常称为穷举路径测试,在本书第4章中将进一步进行深入探讨,在这里就不多加叙述。
所谓穷举路径测试,即如果使用测试用例执行了程序中所有可能的控制流路径,那么程序有可能得到了完全测试。
然而,这个论断存在两个问题。
首先,程序中不同逻辑路径的数最可能达到天文数字。
图2-1所示的小程序显示了这一点。
该图是一个控制流图,每一个结点或圆圈都代表一个按顺序执行的语句段,通常以一个分支语句结束。
每一条边或弧线表示语句段之间的控制(分支)的转换。
图2-1描述的是一个有着10~20行语句的程序,包含一个迭代20次的DO循环。
在DO循环体中,包含一系列嵌套的IF语句。
要确定不同逻辑路径的数量,也相当于要确定从点a~点b之间所有不同路径的数量(假定程序中所有的判断语句都是相互独立的)。
这个数量大约是1014,即100万亿,是从520+519…十51计算而来,5是循环体内的路径数量。
由于大多数的人难以对这个数字有一个直观的概念,不妨设想一下:如果在每五分钟内可以编写、执行和确认一个测试用例,那么需要大约10亿年才能测试完所有的路径。
假如可以快上300倍,每秒就完成一次测试,也得用漫长的320万年才能完成这项工作。
未经同意,严禁以任何形式拷贝
9 第2章软件测试的心理学和经济学 图2-1一个小型程序的控制流图 当然,在实际程序中,判断并非都是彼此独立的,这意味着可能实际执行的路径数量要稍微少一些。
但是,从另一方面来讲,实际应用的程序要比图2-1所描述的简单程序复杂得多。
因此,穷举路径测试就如同穷举输入测试,非但不可能,也是不切实际的。
“穷举路径测试即完全的测试”论断存在的第二个问题是,虽然我们可以测试到程序中的所有路径,但是程序可能仍然存在着错误。
这有三个原因。

一,即使是穷举路径测试也决不能保证程序符合其设计规范。
举例来说,如果要编写一个升序排序程序,但却错误地编成了一个降序排序程序,那么穷举路径测试就没多大价值了;程序仍然存在着一个缺陷:它是个错误的程序因为不符合设计的规范。

二,程序可能会因为缺少某些路径而存在问题。
穷举路径测试当然不能发现缺少了哪些必需路径。
未经同意,严禁以任何形式拷贝 10 软件测试的艺术 第
三,穷举路径测试可能不会暴露数据敏感错误。
这样的例子有很多,举一个简单的例子就能说明问题。
假设在某个程序中要比较两个数值是否收敛,也就是检查两个数值之间的差异是否小于某个既定的值。
比如,我们可能会这样编一条Java语言的IF语句: if(a–b然而,要找出这样的错误,取决于a和b所取的值,而仅仅执行程序中的每条路径并不一定能找出错误来。
总之,尽管穷举输入测试要强于穷举路径测试,但两者都不是有效的方法,因为这两种方法都不可行。
那么,也许存在别的方法,将黑盒测试和白盒测试的要素结合起来,形成一个合理但并不十分完美的测试策略。
本书的第4章将深入讨论这个话题。
2.3软件测试的原则 让我们继续本章的话题基础,即软件测试中大多数重要的问题都是心理学问题。
我们可以归纳出一系列重要的测试指导原则,这些原则看上去大多都是显而易见的,但常常总是被我们忽视掉。
表2-l总结了这些重要原则,每条原则都将在下面的章节中详细介绍。
表2-1软件测试的重要原则 编号12345 6 78910 原则测试用例中一个必需部分是对预期输出或结果进行定义程序员应避免测试自己编写的程序编写软件的组织不应当测试自已编写的软件应当彻底检查每个测试的执行结果测试用例的编写不仅应当根据有效和预料到的输入情况,而且也应当根据无效和未预料到的输入情况检查程序是否“未做其应该做的”仅是测试的一半,测试的另一半是检查程是否“做了其不应该做的”应避免测试用例用后即弃,除非软件本身就是个一次性的软件计划测试工作时不应默许假定不会发现错误程序某部分存在更多错误的可能性,与该部分已发现错误的数量成正比软件测试是一项极富创造性,极具智力的挑战性的工作 未经同意,严禁以任何形式拷贝 11 第2章软件测试的心理学和经济学 原则l:测试用例中一个必需部分是对预期输出或结果的定义。
这条显而易见的原则在软件测试中是最常犯的错误之
一。
同样,这个问题也是基于人们的心理的。
如果某个测试用例的预期结果事先没有得到定义,由于“所见即所想”现象的存在。
某个似是而非、实际上是错误的结果可能会被解释成正确的结论。
换句话说,尽管“软件测试是破坏性”的定义是合理的,但人们在潜意识中仍然渴望看到正确的结果。
克服这种倾向的一种方法、就是通过事先精确定义程序的预期输出,鼓励人们对所有的输出进行仔细检查。
因此,一个测试用例必须包括两个部分:
1.对程序的输入数据的描述。

2.对程序在上述输入数据下的正确输出结果的精确描述。
所谓“问题”,可以归纳为一个或一组我们不能给出可信服的解释、看上去不太正常或不符合我们期望或预想的事实。
应当明确的是,在确定事物存在“问题”之前,人们必须已经形成特定的认识。
没有期望,也就没有所谓的意外。
原则2:程序员应当避免测试自己编写的程序. 任何作者都知道或应该知道,亲自编辑或校对自己的作品确实是个不好的做法。
作者清楚某段文字要说明的是什么,实际表达出来的意思却南辕北辙,而自己可能却意识不到。
况且实际上也不会想在自己的作品中找出什么错误来。
对程序员而言,也存在相同的问题。
如果我们对软件项目关注的重点发生变化,就会产生另外一个问题。
当程序员“建设性”地设计和编写完程序之后,很难让他突然改变视角以一种“破坏性”的眼光来审查程序。
正如许多房屋业主都知道的那样,撕下屋里的墙纸(这是个破坏性的过程)并不容易,如果这些墙纸又恰恰是业主第一个亲手贴的,尤其令其沮丧不已。
同样,大多数程序员都不能有效地测试自己编写的程序,因为他们无法改变思维方式来尽力暴露自己程序中的错误。
另外,程序员可能会下意识地避免找出错误来,担心受到同事、上司、客户或正在开发的程序或系统的主管的惩罚。
仅次于上面的心理学问题,还有一个重要的问题:由干程序员错误地理解了疑难定义或规范,导致程序中存在错误。
如果情况是这样,程序员可能会带着同样的 未经同意,严禁以任何形式拷贝 12 软件测试的艺术 误解来测试自己的程序。
这并不意味着程序员测试自己的程序是不可能的。
当然,我们的言下之意是,让其他人来测试程序会更加有效,也会更容易测试成功。
请注意,我们的论据并不适合于“调试”(纠正已知的错误)。
“调试”由程序的编写人员来完成会有效得多。
原则3:编写软件的组织不应当测试自己编写的软件. 这里的论据与前面的论据相似。
从很多方面来讲,一个软件项目或编程组织是一个有机的机构,具有与个体程序员相似的心理问题。
而且在大多数情况下,主要是根据其在给定时间、特定成本范围内开发软件的能力来衡量编程组织或项目经理。
其中的一个原因是,度量时间和成本目标比较容易,而定量地衡量软件的可靠性则极其困难.即便是合理规划和实施的测试过程,也可能被认为降低了完成进度和成本目标的可能性,因此、编程组织难以客观地测试自己的软件。
同样,我们并不是说编程组织发现程序中的问题是不可能的,事实上很多组织已经在某种程度上成功地做到了这一点。
当然,我们的言下之意是,更经济的方法是由客观、独立的第三方来进行测试。
原则4:应当彻底检查每个测试的执行结果。
这个原则可能是最显而易见的原则,但也同样常常被忽视。
我们见过大量的例子,即便错误的症状在输出清单中可以清楚地看到,但还是没有找出那些错误来。
换言之,在后续测试中发现的错误,往往是前面的测试遗漏掉的。
原则5:测试用例的编写不仅应当根据有效和预期的输入情况,而且也应当根据无效和未预料到的输入情况。
在测试软件时,有一个自然的倾向,即将重点集中在有效和预期的输入情况上,而忽略了无效和未预料到的情况。
比如,在本书第l章三角形程序的测试中,总是出现这个倾向。
例如,很少有人会向程序输入1,2,5以证明程序不会错误地将其解释为一个不规则三角形,而不是一个无效三角形。
此外,在软件产品中突然暴露出来的许多 未经同意,严禁以任何形式拷贝 13 第2章软件测试的心理学和经济学 问题是当程序以某些新的或未预料到的方式运行时发现的。
因此,针对未预料到的和无效输入情况的测试用例,似乎比针对有效输入情况的那些用例更能发现问题。
原则6:检查程序是否“未做其应该做的”仅是测试的一半,测试的另一半是检查程序是否“做了其不应该做的”。
这条原则是上条原则的必然结果。
必须检查程序是否有我们不希望的负作用。
比如,某个工资管理程序即便可以生成正确的工资单,但是如果也为非雇员生成工资单或者它覆盖掉了人员文件的第一条记录,这样的程序仍然是不正确的程序。
原则7:应避免测试用例用后即弃,除非软件本身就是一个一次性的软件。
这个问题在采用交互式系统来测试软件时最常见。
人们通常会坐在终端前,匆忙地编写测试用例,然后将这些用例交由程序执行。
这样做的问题在于,饱含我们宝贵投人的测试用例,在测试结束后就消失了,一旦软件需要重新测试(例如,当改正了某个错误或作了某种改进后),又必须重新设计这些测试用例。
情况往往是这样的,由于重新设计测试用例需要投人大量的工作,人们总是避免这样做。
因此,对该程序的重新测试极少会同上次一样严格。
这就意味着,如果对程序的更改导致了程序某个先前可以执行的部分发生了故障,这个故障往往是不会被发现的,保留测试用例,当程序其他部件发生更动后重新执行,这就是我们所谓的“回归测试”。
原则8:计划测试工作时不应默许假定不会发现错误。
项目经理经常容易犯这个错误,这也是使用了不正确的测试定义的一个迹象——也就是说,假定“测试是一个证明程序正确运行的过程”。
我们再一次重申,所谓测试,就是为发现错误而执行程序的过程。
原则9:程序某部分存在更多错误的可能性,与该部分已发现错误的数目成正比。
这种现象如图2-2所示。
乍看上去,这幅图似乎没有什么意义,但很多程序都存在这种现象。
例如,假如某个程序由两个模块、类或子程序A和B组成,模块A中已经发现了五个错误,而模块B中仅仅找到了一处错误。
如果模块A所经过的测试并不是故意设计得更为严格,那么该原则告诉我们,模块A与模块B相比,存在更多错误的可能性要大。
未经同意,严禁以任何形式拷贝 14 软件测试的艺术 该原则的另一个说法是,错误总是倾向于聚集存在,而在一个具体的程序中,某些部分要比其他部分更容易存在错误,尽管没有人能够对这种现象给出很好的解释。
这种现象之所以有用.是因为它给予了我们对软件测试过程的洞察或反馈。
如果一个程序的某个部分远比其他部分更容易产生错误.那么这种现象告诉我们,为了使测试获得更大的成效,最好对这些容易存在错误的部分进行额外的测试。
图2-2残存错误与已知错误间令人惊奇的联系原则10:软件测试是一项极富创造性、极具智力挑战性的工作。
测试一个大型软件所需要的创造性很可能超过了开发该软件所需要的创造性.我们已经看到,要充分地测试一个软件以确保所有错误都不存在是不可能的。
本书后续章节讨论的技术使我们能够为某个软件设计出合理的测试用例集,然而这些技术仍然需要大量的创造性。
2.4小结 在阅读本书接下来的内容时,请牢记以下三个重要的测试原则:•软件测试是为发现错误而执行程序的过程。
•一个好的测试用例具有较高的发现某个尚未发现的错误的可能性。
•一个成功的测试用例能够发现某个尚未发现的错误。
未经同意,严禁以任何形式拷贝 15 第3章代码检查、走查与评审 第3章代码检查、走查与评审 多年以来,软件界的大多数人都持有一个想法,即编写程序仅仅是为了提供给机器执行,并不是供人们阅读的,软件测试的惟一方法就是在计算机上执行它。
20世纪70年代早期,一些程序员最先意识到阅读代码对于构成完善的软件测试和调试手段的价值,通过他们的努力,原有的观念开始发生变化。
今天,并不是所有的软件测试人员都要阅读代码,但是研读程序代码作为测试工作的一部分,这个观念已经得到了广泛认同。
以下几个因素会影响到特定的测试和调试工作需要人工实际阅读代码的可能性:软件的规模和复杂度、软件开发团队的规模、软件开发的时限(例如时间安排表是松散还是紧密)等,当然还有编程小组的技术背景和文化。
基于这些原因,在深入研究较为传统的基于计算机的测试技术之前.我们首先讨论非基于计算机测试的过程(即“人工测试”)。
人工测试技术在查找错误方面非常有效,以至于任何编程项目都应该使用其中的一种或多种技术。
应该在程序开始编码之后、基于计算机的测试开始之前使用这些方法。
同样,也可以在编程过程的更早阶段就开始设计和应用类似的方法(例如在每个设计阶段的末尾),但是这些内容超出了本书讨论的范围。
在开始讨论人工测试技术之前,有一条重要的注意事项:由于包含了人为因素在内,导致很多方法的正规性要差于由计算机执行的数学证明,人们可能会怀疑某些如此简单和不正规的东西是否有用。
反之亦然。
这些不正规的方法并没有妨碍测试取得成功;相反,它们从以下两个方面显著地提高了测试的功效和可靠性。
首先,人们普遍认识到错误发现得越早,改正错误的成本越低,正确改正错误的可能性也越大。
其次,程序员在开始基于计算机的测试时似乎要经历一个心理上的转变。
从内部产生的压力似乎会急剧增长,并产生一个趋势,要“尽可能快地修正这个缺陷”。
由于这些压力的存在.程序员在改正某个由基于计算机测试发现的错误时所犯的失误,要比改正早期发现的问题时所犯的失误更多一些。
未经同意,严禁以任何形式拷贝 16 软件测试的艺术 3.1检查与走查(InspectionsAndWalkthroughs) 代码检查与走查是两种主要的人工测试方法。
由于这两种方法具有很多的共同之处,在这里我们将一起讨论它们的相似点,而它们的不同之处将在后续章节中进行介绍。
代码检查与走查都要求人们组成一个小组来阅读或直观检查特定的程序。
无论采用哪种方法,参加者都需要完成一些准备工作。
准备工作的高潮是在参加者会议上进行的所谓“头脑风暴会”。
“头脑风暴会”的目标是找出错误来,但不必找出改正错误的方法。
换句话说,是测试,而不是调试。
代码检查与走查已经广泛运用了很长时间。
我们认为,它们的成功与本文第2章所述的那些原则有关。
在代码走查中,一组开发人员(三至四人为最佳)对代码进行审核。
参加者当中只有一人是程序编写者。
因此,软件测试的主要工作是由其他人,而不是软件编写者本人来完成.这符合“软件编写者往往不能有效地测试自己编写的软件”的测试原则。
代码检查与走查是对过去桌面检查过程(在提交测试前由程序员阅读自己程序的过程)的改进。
与原方法相比,代码检查与走查更为有效,同样是因为在实施过程中,除了软件编写者本人,还有其他人参与进来。
代码走查的另一个优点在于,一旦发现错误,通常就能在代码中对其进行精确定位,这就降低了调试(错误修正)的成本。
另外,这个过程通常发现成批的错误。
这样错误就可以一同得到修正。
而基于计算机的测试通常只能暴露出错误的某个表症(程序不能停止,或打印出一个无意义的结果),错误通常是逐个地被发现并得到纠正的。
在典型的程序中,这些方法通常会有效地查找出30%~70%的逻辑设计和编码 错误。
但是,这些方法不能有效地查找出高层次的设计错误,例如在软件需求分析 阶段的错误。
请注意,所谓30%~70%的错误发现率,并不是说所有错误中多达 70%可能会被找出来,而是讲这些方法在测试过程结束时可以有效地查找出多达70 %的已知错误。
请记住,第2章告诉我们,程序中的错误总数始终是未知的。
未经同意,严禁以任何形式拷贝 17 第3章代码检查、走查与评审 当然,可能存在对这统计数字的批评,即人工方法只能发现“简单”的错误(即与基于计算机的测试方法相比,所发现的问题显得微不足道),而困难的、不明显的或微妙的错误只能用基于计算机的测试方法才能找到。
然而,一些测试人员在使用了人工方法之后发现,对于某些特定类型的错误,人工方法比基于计算机的方法更有效,而对于其他错误类型,基于计算机的方法更有效。
这就意味着,代码检查/走查与基于计算机的测试是互补的。
缺少其中任何一种,错误检查的效率都会降低。
最后,不但这些测试过程对于测试新开发的程序有着不可估量的作用,而且对于测试更改后的程序,这些测试过程具有相同的作用,甚至更大。
根据我们的经验,修改一个现存的程序比编写一个新程序更容易产生错误(以每写一行代码的错误数量计)。
因此,除了回归测试方法之外,更改后的程序还要进行这些人工方法的测试。
3.2代码检查(CodeInspections) 所谓代码检查是以组为单位阅读代码,它是一系列规程和错误检查技术的集合。
对代码检查的大多数讨论都集中在规程、所要填写的表格等。
这里对整个规程进行简短的概述,之后我们将重点讨论实际的错误检查技术。
一个代码检查小组通常由四人组成,其中一人发挥着协调作用。
协调人应该是个称职的程序员,但不是该程序的编码人员,不需要对程序的细节了解得很清楚。
协调人的职责包括以下几点: •为代码检查分发材料、安排进程。
•在代码检查中起主导作用。
•记录发现的所有错误。
•确保所有错误随后得到改正。
协调人就像质量控制工程师。
小组中的第二个成员是该程序的编码人员。
小组中的其他成员通常是程序的设计人员(如果设计人员不同于编码人员的话),以及一名测试专家。
在代码检查之前的几天,协调人将程序清单和设计规范分发给其他成员。
所有成员应在检查之前熟悉这些材料。
在检查进行时,主要进行两项活动:
1.由程序编码人员逐条语句讲述程序的逻辑结构。
在讲述的过程当中,小组 未经同意,严禁以任何形式拷贝 18 软件测试的艺术 的其他成员应提问题、判断是否存在错误。
在讲述中,很可能是程序编码人员本人而不是其他小组成员发现了大部分错误。
换句话说,对着大家大声朗读程序,这种简单的做法看来是一个非常有效的错误检查方法。

2.对着历来常见的编码错误列表分析程序(该列表将在下一节中介绍)。
协调人负责确保检查会议的讨论高效地进行、每个参与者都将注意力集中于查找错误而不是修正错误(错误的修正由程序员在检查会议之后完成)。
会议结束之后,程序员会得到一份已发现错误的清单。
如果发现的错误太多,或者某个错误涉及对程序做根本的改动,协调人可能会在错误修正后安排对程序进行再次检查。
这份错误清单也要进行分析,归纳,用以提炼错误列表,以便提高以后代码检查的效率。
如上所述,这个代码检查过程通常将注意力集中在发现错误上,而不是纠正错误。
然而,有些小组可能会发现,当检查出某个小问题之后,有两三个人(包括负责该代码的程序员本人)会建议对设计进行明显的修补以解决这个特例。
那么,对这个小问题的讨论,反过来会将整个小组的注意力集中在设计的某个部分。
在探讨修补设计来解决这个小问题的最佳方法时,有人可能会注意到另外的问题。
既然小组已经发现了设计中同一部分的两个相关问题,那么每隔几段代码就可能需要密集的注释。
几分钟之内,整个设计就被彻底检查完,任何问题都会一目了然。
在代码检查的时间及地点的选择上,应避免所有的外部干扰。
代码检查会议的理想时间应在90~120分钟之间。
由于开会是一项繁重的脑力劳动,会议时间越长效率越低。
大多数的代码检查都是按每小时大约阅读150行代码的速度进行。
因此,对大型软件的检查应安排多个代码检查会议同时进行,每个代码检查会议处理一个或几个模块或子程序。
请注意,要使检查过程有成效,必须树立正确的态度。
如果程序员将代码检查 视为对其人格的攻击、采取了防范的态度,那么检查过程就不会有效果。
正确的做 法是,程序员必须怀着非自我本位的态度来对待检查过程,对整个过程采取积极和 建设性的态度:代码检查的目标是发现程序中的错误,从而改进软件的质量。
正因 为这个原因,大多数人建议应对代码检查的结果进行保密,仪限于参与者范围内部。
尤其是如果管理人员想利用代码检查的结果,那么就与检查过程的目的背道而驰 未经同意,严禁以任何形式拷贝 19 第3章代码检查、走查与评审 了。
除了可以发现错误这个主要作用之外.代码检查还有几个有益的附带作用。

一,程序员通常会得到编程风格、算法选择及编程技术等方面的反馈信息。
其他参与者也可以通过接触其他程序员的错误和编程风格而同样受益匪浅。
还有,代码检查还是早期发现程序中最易出错部分的方法之
一,有助于在基于计算机的测试过程中将更多的注意力集中在这些地方(本文第2章中的测试原则之一)。
3.3用于代码检查的错误列表 代码检查过程的一个重要部分就是对照一份错误列表,来检查程序是否存在常见错误。
遗憾的是,有些错误列表更多地注重编程风格而不是错误(例如,“注释是否准确且有意义?”,“if-else代码段和do-while代码段是否缩进对齐?”),错误检查太过模糊而实际上没有用(例如,“代码是否满足设计需求?”)。
本节中讨论的错误列表是经多年对软件错误的研究编辑而成的。
该错误列表在很大程度上是独立于编程语言的,也就是说,大多数的错误都可能出现在用任意语言编写的程序中。
读者可以把自己使用的编程语言中特有的错误,以及代码检查发现的错误补充到这份错误列表中去。
3.3.1数据引用错误
1.是否有引用的变量未赋值或未初始化?这可能是最常见的编程错误,在各种环境中都可能发生。
在引用每个数据项(如变量、数组元素、结构中的域)时,应试图非正式地“证明”该数据项在当前位置具有确定的值。

2.对于所有的数组引用,是否每一个下标的值都在相应维规定的界限之内?
3.对于所有的数组引用,是否每一个下标的值都是整数?虽然在某些语言中这不是错误,但这样做是危险的。

4.对于所有的通过指针或引用变量的引用,当前引用的内存单元是否分配?这就是所谓的“虚调用(danglingreference)”错误。
当指针的生命期大于所引用内存单元的生命期时,错误就会发生。
当指针引用了过程中的一个局部变量,而指针的 未经同意,严禁以任何形式拷贝 20 软件测试的艺术 值又被赋给一个输出参数或一个全局变量,过程返回(释放了引用的内存单元)结束,而后程序试图使用指针的值时,这种错误就会发生。
与前面检查错误的方法类似,应试图非正式地“证明”,对于每个使用指针值的引用,引用的内存单元者都存在。

5.如果一个内存区域具有不同属性的别名,当通过别名进行引用时,内存区域中的数据值是否具有正确的属性?在FORTRAN语言中对EQUIVALENCE语句使用,或COBOL语言中对REDEFINES语句使用的地方,都可能发生这种错误。
例如,一个FORTRAN语言程序包含一个实型变量A和一个整型变量
B,两者都通过使用EQUIVALENCE语句而成为同一内存区域的别名。
如果程序先对A赋值,然后又引用变量
B,由于机器可能会将内存中用浮点位表示的实数当作整数,在这种情况下错误就可能发生。

6.变量值的类型或属性是否与编译器所预期的一致?当
C、C++或COBOL程序将某个记录读到内存中,并使用一个结构来引用它时,由于记录的物理表示与结构定义存在差异,这种情况下错误就可能发生。

7.在使用的计算机上,当内存分配的单元小于内存可寻址的单元大小时,是否存在直接或间接的寻址错误?例如,在某些条件下,定长的位串不必以字节边界为起点,但是地址又总是指向字节边界的。
如果程序计算一个位串的地址,稍后又通过该地址引用这个位串,可能会指向错误的内存位置。
将一个位串参数传送给一个子程序时,也可能发生这种情况。

8.当使用指针或引用变量时,被引用的内存的属性是否与编译器所预期的一致?这种错误的一个例子是,当一个指向某个数据结构的C++指针,被赋值为另外的数据结构的地址。

9.假如一个数据结构在多个过程或子程序中被引用,那么每个过程或子程序对该结构的定义是否都相同。
10.如果字符串有索引,当对数组进行索引操作或下标引用,字符串的边界取值是否有“仅差一个(off-by-one)”的错误? 11.对于面向对象的语言,是否所有的继承需求都在实现类中得到了满足? 未经同意,严禁以任何形式拷贝 21 第3章代码检查、走查与评审 3.3.2数据声明错误
1.是否所有的变量都进行了明确的声明?没有明确声明虽然不一定是错误,但通常却是麻烦的源头。
举例来说,如果一个程序的子程序接收一个数组参数,却未将该参数定义为数组(如用DIMENSION语句),对该数组的引用(如C=A(I))会被解释为一个函数调用,导致计算机试图将此数组当作程序执行。
另外,如果某个变量在一个内部过程或程序块中没有明确声明,是否可以理解为该变量在这个程序块中被共用?
2.如果变量所有的属性在声明中没有明确说明,那么默认的属性能否被正确理解?举例来说,在Java语言中,程序接收到的默认属性往往是导致意外情况发生的源头。

3.如果变量在声明语句中被初始化,那么它的初始化是否正确?在很多语言中,数组和字符串的初始化比较复杂,因此也成为容易出错的地方。

4.是否每个变量都被赋予了正确的长度和数据类型?
5.变量的初始化是否与其存储空间的类型一致?举例来说,如果FORTRAN语言子程序中的一个变量在每次调用子程序时都需要重新初始化一次,那么必须使用赋值语句对其初始化,而不应该用DATA语句。

6.是否存在着相似名称的变量(如VOLT和VOLTS)?这种情况不一定是错误,但应被视为警告,这些名称可能会在程序中发生混淆。
3.3.3运算错误
1.是否存在不一致的数据类型(如非算术类型)的变量间的运算?
2.是否有混合模式的运算?例如,将浮点变量与一个整型变量做加法运算。
这 种情况并不一定是错误,但应该谨慎使用.确保程序语言的转换规则能够被正确理 解。
看看下面的Java程序片段.显示了整数运算中可能发生的取整误差: intx=1; inty=2; intz=0; z=x/y; 未经同意,严禁以任何形式拷贝 22 软件测试的艺术 System.out.println("z="+z); OUTPUT:z=03.是否有相同数据类型不同字长变量间的运算?
4.赋值语句的目标变量的数据类型是否小于右边表达式的数据类型或结果?
5.在表达式的运算中是否存在表达式向上或向下溢出的情况,也就是说,最终的结果看起来是个有效值,但中间结果对于编程语言的数据类型可能过大或过小。

6.除法运算中的除数是否可能为0?
7.如果计算机表达变量的基本方式是基于二进制的,那么运算结果是否不精确?也就是说,在一个二进制计算机上,10×0.l很少会等于l.0。

8.在特定场合,变量的值是否超出了有意义的范围?例如,对变量PROBABILITY赋值的语句可能需要进行检查,保证赋值始终为正且不大丁1.0。

9.对于包含一个以上操作符的表达式,赋值顺序和操作符的优先顺序是否正确? 10.整数的运算是否有使用不当的情况,尤其是除法?举例来说.如果i是一个整型变量,表达式2*i/2==i是否成立,取决于i是奇数还是偶数,或是先运算乘法,还是先运算除法。
3.3.4比较错误
1.是否有不同数据类型的变量之间的比较运算,例如,将字符串与地址、日期或数字相比较?
2.是否有混合模式的比较运算,或不同长度的变量间的比较运算?如果有,应确保程序能正确理解转换规则。

3.比较运算符是否正确?程序员经常混淆“至多”、“至少”、“大于”、“不小于”、“小于”和“等于”等比较关系。

4.每个布尔表达式所叙述的内容是否都正确?在编写涉及“与”、“或”或“非” 未经同意,严禁以任何形式拷贝 23 第3章代码检查、走查与评审 的表达式时,程序员经常犯错。

5.布尔运算符的操作数是否是布尔类型的?比较运算符和布尔运算符是否错误地混住了一起?这是一类经常会犯的错误。
这里我们描述几个典型错误的例子。
如果想判断i是否在2~10之间,表达式2如果想判断i是否大于x或y,表达i>x||y也是不正确的,正确的应该是(i>x)||(i>y)。
如果要比较三个数字是否相等,表达式if(a==b==c)的实际意思却大相径庭。
如果需要验证数学关系x>y>z,正确的表达式应该是(x>y)&&(y>z)。

6.在二进制的计算机上,是否有用二进制表示的小数或浮点数的比较运算?由于四舍五入,以及用二进制表示十进制数的近似度,这往往是错误的根源。

7.对于那些包含一个以上布尔运算符的表达式,赋值顺序以及运算符的优先顺序是否正确?也就是说,如果碰到如同(if((a==2)&&(b==2)||(c==3))的表达式,程序能否正确理解是“与”运算在先还是“或”运算在先?
8.编译器计算布尔表达式的方式是否会对程序产生影响?例如,语句if((x==0&&(x/y)>z)对于有的编译器来说是可接受的,因为其认为一旦“与”运算符的一侧为FALSE时,另一侧就不用计算;但是对于其他编译器来说,却可能引起一个被0除的错误。
3.3.5控制流程错误
1.如果程序包含多条分支路径,比如有计算GOTO语句,索引变量的值是否会大于可能的分支数量?例如,在语句 GOTO(200,300,400),i中,i的取值是否总是1、2或3?
2.是否所有的循环最终都终止了?应设计一个非正式的证据或论据来证明每一个循环都会终止。

3.程序、模块或子程序是否最终都终止了?
4.由于实际情况没有满足循环的入口条件,循环体是否有可能从未执行过?如 未经同意,严禁以任何形式拷贝 24 软件测试的艺术 果确实发生这种情况,这里是否是一处疏漏?例如,如果循环以下面的语句作为开头. for{i==x;i<=z;i++}{...}while(NOTFOUND){...}当NOTFOUND初始时就为假,或者x大于z时,情况会如何呢’
5.如果循环同时由迭代变量和一个布尔条件所控制(如一个搜索循环),如果循环越界(fall-through)了,后果会如何?例如,伪指令循环以 D0I=1toTABLESIZEWHILE(NOTFOUND)开头,如果NOTFOUND永不为假,会发生什么结果呢?
6.是否存在“仅差一个”的错误,如迭代数量恰恰多一次或少一次?这在从0开始的循环中是常见的错误。
我们会经常忘记将“0”作为一次计数。
举例来说,如果想编写一段Java代码执行10次循环,下面的语句是错误的,因为它执行了11次: for(inti=0;i<=10;i++){System.out.println(i); } 正确的应该是执行10次循环for(inti=0;i<=9;i++){ System.out.println(i);}
7.如果编程语言中有语句组或代码块的概念(例如do-while或{...}),是否每一组语句都有一个明确的while语句,并且do语句也与其相应的语句组对应?或者,是否每一个左括号都对应有一个右括号?目前的大多数编译器都能识别出这些不匹配的情况。

8.是否存在不能穷尽的判断?举例来说,如果一个输入参数的预期值是1,2或
3,当参数值不为l或2时,在逻辑上是否假设了参数必定为3?如果是这样的话,这种假设是否有效? 未经同意,严禁以任何形式拷贝 25 第3章代码检查、走查与评审 3.3.6接口错误
1.被调用模块接收到的形参(parameter)数量是否等于调用模块发送的实参(argument)数量?另外,顺序是否正确?
2.实参的属性(如数据类型和大小)是否与相应形参的属性相匹配?
3.实参的量纲是否与对应形参的量纲相匹配?举例来说,是否形参以度为单位而实参以弧度为单位?
4.此模块传递给被模块的实参数量,是否等于被模块期望的形参数量?
5.此模块传递给彼模块的实参的属性,是否与彼模块相应形参的属性相匹配?
6.此模块传递给彼模块的实参的量纲,是否与彼模块相应形参的量纲相匹配?
7.如果调用了内置函数,实参的数量,属性,顺序是否正确?
8.如果某个模块或类有多个入口点,是否引用了与当前入口点无关的形参?下面PL/1程序的第二个赋值语句就存在这种错误 A:PROCEDURE(
W,X);W=X+1;RETURNB:ENTRY(
Y,Z);Y=X+Z;END;
9.是否有子程序改变了某个原本仅为输入值的形参? 10.如果存在全局变量.在所有引用它们的模块中,它们的定义和属性是否相同? 11.常数是否以实参形式传递过?在一些用FORTRAN语言编写的程序中,诸如 CALLSUBX(J,3)的语句是很危险的,因为如果子程序SUBX对其第二个形参进行赋值,常数3的值将会被改变。
未经同意,严禁以任何形式拷贝 26 软件测试的艺术 3.3.7输入/输出错误
1.如果对文件明确声明过,其属性是否正确?
2.打开文件的语句中各项属性的设置是否正确?
3.格式规范是否与I/O语句中的信息相吻合?举例来说,在FORTRAN语言中,是否每个FORMAT语句都与相应的READ或WRITE语句相一致(就各项的数量和属性而言)?
4.是否有足够的可用内存空间,来保留程序将读取的文件?
5.是否所有的文件在使用之前都打开?
6.是否所有的文件在使用之后都关闭了?
7.是否判断文件结束的条件,并正确处理?
8.对I/O出错情况处理是否正确?
9.任何打印或显示的文本信息中是否存在拼写或语法错误? 3.3.8其他检查
1.如果编译器建立了一个标识符交叉引用列表,那么对该列表进行检查,查看是否有变量从未引用过,或仅被引用过一次。

2.如果编译器建立了一个属性列表,那么对每个变量的属性进行检查,确保没有赋予过不希望的默认属性值。

3.如果程序编译通过了,但计算机提供了一个或多个“警告”或“提示”信息,应对此逐一进行认真检查。
“警告”信息指出编译器对程序某些操作的正确性有所怀疑,所有这些疑问都应进行检查。
“提示”信息可能会罗列山没有声明的变量,或者是不利于代码优化的用法。

4.程序或模块是否具有足够的鲁棒性?也就是说,它是否对其输入的合法性进行了检查? 未经同意,严禁以任何形式拷贝 27 第3章代码检查、走查与评审
5.程序是否遗漏了某个功能? 这些检查列表在表3-l和表3-2中进行了总结。
表3-1代码检查错误列表总结第一部分 数据引用错误
1.是否有引用的变量未赋值或未初始化?
2.下标的值是否在范围之内?
3.是否存在非整数下标?
4.是否存在虚调用?
5.当使用别名时属性是否正确?
6.记录和结构的属性是否匹配?
7.是否计算位串的地址?是否传递位串参数?
8.基础的存储属性是否正确?
9.跨过程的结构定义是否匹配?10.索引或下标操作是否有“仅差一个”的错误?11.继承需求是否得到满足? 运算错误
1.是否存在非算术变量间的运算?
2.是否存在混合摸式的运算?
3.是否存在不同字长变量问的运算?
4.目标变量的大小是否小于赋值大小?
5.中间结果是否上溢或下溢?
6.是否存住被0除?
7.是否存在二进制的不精确度?
8.变量的值是否超过了有意义的范围?
9.操作符的优先顺序是否被正确理解?10.整数除法是否正确? 数据声明错误
1.是否所有的变量都已声明?
2.默认的属性是否被正确理解?
3.数组和字符串的初始化是否正确?
4.变量是否赋予了正确的长度,类型和存储类?
5.初始化是否与存储类相一致?
6.是否有相似的变量名? 比较错误
1.是否存在不同类型变量间的比较?
2.是否存在混合模式的比较运算?
3.比较运算符是否正确?
4.布尔表达式是否正确?
5.比较运算是是否与布尔表达式相混合?
6.是否存在二进制小数的比较?
7.操作符的优先顺序是否被正确理解?
8.编译器对布尔表达武的计算方式是否被正确理解? 表3-2代码检查错误列表总结第二部分 控制流程错误
1.是否超出了多条分支路径?
2.是否每个循环都终止了?
3.是否每个程序都终止了?
4.是否存在由于入口条件不满足而跳过循环体?
5.可能的循环越界是否正确?
6.是否存在“仅差一个”的迭代错误? 输入/输出错误
1.文件的属性是否正确?2.0PEN语句是否正确?
3.I/O语句是否符合格式规范?
4.缓冲大小与记录大小是否匹配?
5.文件在使用前是否打开?
6.文件在使用后是否关闭? 未经同意,严禁以任何形式拷贝 28 软件测试的艺术
7.DO/END语句是否匹配?
8.是否存在不能穷尽的判断?
9.输出信息中是否有文字或语法错误?
7.文件结束条件是否被正确处理?
8.是否处理了I/O错误? 接口错误
1.形参的数量是否等于实参的数量?
2.形参的量纲是否与实参的量纲相匹配?
3.形参的量纲是否与实参的量纲相匹配?
4.传递给被调用模块的实参个数是否等于其形参个数?
5.传递给被调用模块的实参属性是否与其形参属性匹配?
6.传递给被调用模块的实参量纲是否与其形参量纲匹配?
7.调用内部函数的实参的数量、属性,顺序是否正确?
8.是否引用了与当前入口点无关的形参?
9.是否改变了某个原本仅为输入值的形参?10.全局变量的定义在模块间是否一致?11.常数是否以实参形式传递过? 其他检查
1.在交叉引用列表中是否存在未引用过的变量?
2.属性列表是否与预期的相一致?
3.是否存在“警告”或“提示”信息?
4.是否对输入的合法性进行了检查?
5.是否遗漏了某个功能? 3.4代码走查(Walkthroughs) 代码走查与代码检查很相似,都是以小组为单位进行代码阅读,是一系列规程和错误检查技术的集合。
代码走查的过程与代码检查大体相同,但是规程稍微有所不同,采用的错误检查技术也不一样。
就像代码检查一样,代码走查也是采用持续一至两个小时的小间断会议的形式。
代码走查小组由三至五人组成,其中一个人扮演类似代码检查过程中“协调人”的角色,一个人担任秘书(负责记录所有查出的错误)的角色,还有一个人担任测试人员。
关于这二到五个人的组成结构,有各种各样的建议。
当然,程序员应该是其中之
一。
我们建议另外的参与者应该包括:
(1)一位极富经验的程序员;
(2)一位程序设计语言专家;
(3)一位程序员新手(可以给出新颖,不带偏见的观点),
(4)最终将维护程序的人员;
(5)一位来自其他不同项目的人员;
(6)一位来自该软件编程小组的程序员。
未经同意,严禁以任何形式拷贝 29 第3章代码检查、走查与评审 开始的过程与代码检查相同:参与者在走查会议之前的几天得到材料,他们可以专心钻研程序。
然而走查会议的程则不相同。
不同于仅阅读程序或使用错误检查列表,代码走查的参与者“使用了计算机”。
被指定为测试人员的那个人会带着一些书面的测试用例(程序或模块具有代表性的输入集及预期的输出集)来参加会议。
在会议期间,每个测试用例都在人们脑中进行推演。
也就是说,把测试数据沿程序的逻辑结构走一遍。
程序的状态(如变量的值)记录在纸张或白板上以供监视。
当然,这些测试用例必须结构简单、数量较少,因为人脑执行程序的速度比计算机执行程序的速度慢上若干量级。
因此,这些测试试用例本身并不起到关键的作用;相反,它们的作用是提供了启动代码走查和质疑程序员逻辑思路及其设想的手段。
在大多数的代码走查中,很多问题是在向程序员提问的过程中发现的,而不是由测试用例本身直接发现的。
与代码检查相同,代码走查参与者所持的态度非常关键。
提出的建议应针对程序本身,而不应针对程序员。
换句话说,软件中存在的错误不应被视为编写程序的人员自身的弱点。
相反,这些错误应被看作是伴随着软件开发的艰难性所固有的。
与代码检查过程中描述的相似,代码走查应该有一个后续过程。
同样,代码检查所带来的附带作用(如可以发现易出错的程序区域,通过接触软件错误、编程风格和方法来获得教育等)同样也会发生在代码走查过程中。
3.5桌面检查(DeskChecking) 人工查找错误的第二种过程是古老的桌面检查方法。
桌面检查可视为由单人进行的代码检查或代码走查:由一个人阅读程序,对照错误列表检查程序,对程序推演测试数据。
对于大多数人而言,桌面检查的效率是相当低的。
其中的一个原因是,它是一个完全没有约束的过程。
另一个重要的原因是它违反了本书第2章提出的测试原则,即人们一般不能有效地测试自己编写的程序。
因此桌面检查最好由其他人而非该程序的编写人员来完成(例如,两个程序员可以相互交换各自的程序,而不是桌面检查自己的程序)。
但是即使这样,其效果仍然逊色于代码走查或代码检查。
原因在于代码检查和代码走查小组中存在着互相促进的效应。
小组会议培养了良性竞争的 未经同意,严禁以任何形式拷贝 30 软件测试的艺术 气氛,人们喜欢通过发现问题来展示自己的能力。
而在桌面检查中,由于没有其他人可供展示,也就缺乏这个显而易见的良好效应。
简而言之,桌面检查胜过没有检查,但其效果远远逊色于代码检查和代码走查。
3.6同行评分(PeerRatings) 最后一种人工评审方法与程序测试并无关系(其目标不是为了发现错误),却仍在这里谈到,这是因为它与代码阅读的思想有关。
同行评分是一种依据程序整体质量,可维护性、可扩展性、易用性和清晰性对匿名程序进行评价的技术。
该项技术的目的是为程序员提供自我评价的手段。
选出一位程序员来担任这个评分过程的管理员,管理员又会挑选出大约6~20名参与者(保持匿名性,6人是最少数量)这些参与者都应具备相似的背景(如,不能把Java应用程序员与汇编语言系统程序员编为一组)要求每名参与者都挑选出两个由自己编写的程序以供评审。
其中的一个程序应是参与者自认为能代表其自身能力的最好作品,而另一个则是参与者自认为质量较差的作品。
当所有的程序都收集完毕后,就将这些程序随机分发给参与者。
每名参与者拿到4个程序进行评审,其中的两个是“最好”的程序,另外两个则是相对“较差”的程序,但评审人自己并不知道。
每名参与者每评审一个程序得花费30分钟,评审完后填写一张评价表。
所有4个程序都评审完后,参与者对4个程序的相对质量进行分级。
评价表要求评审人用从1~7的分值(代表明确的“是”,7代表明确的“否”)对诸如下面的问题进行回答: •程序是否易于理解?•高层次的设计是否可见且合理?•低层次的设计是否可见且合理?•修改此程序对评审者而言是否容易?•评审者是否会以编写出该程序而骄傲? 还要要求评审人给出总的评价和建议的改进意见。
评审结束之后,参与者会收到自己的那两个程序的匿名评价表,此外还会收到 一个带统计的总结,说明在所有的程序中其程序的整体和具体得分情况,以及他对 未经同意,严禁以任何形式拷贝 31 第3章代码检查、走查与评审 其他程序的评价与其他评审人对同一程序打分的比较分析情况。
同行评分的目的,是让程序员对自身的编程技术进行自我评价。
同样,该过程适用于企业开发和课堂教学环境。
3.7小结 本章讨论了软件开发人员通常不会考虑到的一种测试形式——人工测试。
大多数人都以为,因为程序是为了供机器执行而编写的,那么也该由机器来对程序进行测试。
这种想法是有问题的。
人工测试方法在暴露错误方面是很有成效的。
实际上,大多数的软件项目都应使用到以下的人工测试方法: •利用错误列表进行代码检查。
•小组代码走查。
•桌面检查。
•同行评审。
未经同意,严禁以任何形式拷贝 32 软件测试的艺术 第4章测试用例的设计 除了第2章探讨的软件测试的心理学问题以外,软件测试中最重要的因素是设计和生成有效的测试用例。
然而,无论软件测试进行得如何具有创造性、如何完全,也不能保证软件中不存在任何错误。
测试用例的设计如此重要,原因在于完全的测试是不可能的,对任何程序的测试必定是不完全的。
那么,最显然的测试策略就是努力使测试尽可能完全。
由于时间和成本的约束,软件测试的最关键问题是: 在所有可能的测试用例中,哪个子集最有可能发现最多的错误? 对软件测试用例设计方法的研究为这个问题提供了答案。
一般而言,在所有的方法中效率最低的是随机输入测试,即在所有可能的输入值中随机选取某个子集来对程序进行测试的过程。
就发现最多错误的可能性而言,随机选取而产生的测试用例集很少有可能是理想的或接近理想的子集。
在本章中,我们将提出一套思考过程,该过程有助于更加睿智地选取测试数据。
本书第2章已经证明穷举的黑盒和白盒测试通常都是不可能的,但同时也建议:将这两种测试的要素组合起来得到一种合理的测试策略。
本章将对这种策略进行研究。
我们可以通过使用特定的面向黑盒测试的测试用例设计方法,而后使用白盒测试方法对程序的逻辑结构进行检查以补充这些测试用例,借此来设计出一个相当严格的测试。
本章将要讨论的测试方法如下: 黑盒测试等价类划分边界值分析因果图分析错误测试 白盒测试语句覆盖判定覆盖条件覆盖判定/条件覆盖多重条件覆盖 未经同意,严禁以任何形式拷贝 33 第4章测试用例的设计 尽管上述方法将分开来进行讨论,但我们建议综合最多的(如果不能是全部的话)测试方法来设计严格的程序测试,因为每一种测试方法都有其独特的优势和弱点。
举例来说,某种方法遗漏掉的错误,而用其他的方法就可能找出来。
没有人曾承诺说:软件测试会是容易的事。
引用一位智者的话,“如果你觉得设计和编写程序很困难,你就并非一无所知。
” 我们推荐的步骤是先使用黑盒测试方法来设计测试用例,然后视情况需要使用白盒测试方法来设计补充的测试用例。
下面首先讨论较为有名的白盒测试方法。
4.1白盒测试(White-BoxTesting) 4.1.1逻辑覆盖测试(Logic-CoverageTesting) 白盒测试关注的是测试用例执行的程度或覆盖程序逻辑结构(源代码)的程度。
如同我们在本书第2章所看到的,完全的白盒测试是将程序中每条路径都执行到,然而对一个带有循环的程序来说,完全的路径测试并不切合实际。
图4-1被测试的小程序 如果完全从路径测试中跳出来看,那么有价值的目标似乎就是将程序中的每条 语句至少执行一次。
遗憾的是,这恰是合理的白盒测试中较弱的准则。
图4-1描述 未经同意,严禁以任何形式拷贝 34 软件测试的艺术 了这种思想。
假设图4-l代表了一个将要进行测试的小程序,其等价的Java代码段如下: Publicvoidfoo(inta,intb,intx){if(a>1&&b==0){x=x/a;}if(a==2||x>1){x=x+1;} }通过编写单个的测试用例遍历程序路径ace,可以执行到每一条语句。
也就是说,通过在点a处设置A=
2,B=
0,X=
3,每条语句将被执行一次(实际上,X可被赋任何值)。
遗憾的是,这个准则相当不足。
举例来说,也许第一个判断应是“或”,而不是“与”。
如果这样,这个错误就会发现不到。
另外,可能第二个判断应该写成“X>0”,这个错误也不会被发现。
还有,程序中存在一条X未发生改变的路径(路径abd),如果这是个错误,它也不会被发现。
换句话说,语句覆盖这条准则有很大的不足,以至于它通常没有什么用处。
判定覆盖或分支覆盖是较强一些的逻辑覆盖准则。
该准则要求必须编写足够的测试用例,使得每一个判断都至少有一个为“真”和为“假”的输出结果。
换句话说,也就是每条分支路径都必须至少遍历一次。
分支或判定语句的例子包括switch,do-while和if-else语句.在一些程序语言如FORTRAN中,多重选择GOTO语句也是合法的。
判定覆盖通常可以满足语句覆盖。
由于每条语句都是在要么从分支语句开始,要么从程序入口点开始的某条子路径上,如果每条分支路径都被执行到了,那么每条语句也应该被执行到了。
但是,仍然还有至少三种例外情况: •程序中不存在判断。
•程序或子程序/方法有着多重入口点。
只有从程序的特定入口点进入时,某 条特定的语句才能执行到。
•在ON单元(ON-unit)里的语句。
遍历每条分支路径并不一定能确保所有 的ON单元都能执行到。
由于我们将语句覆盖视为一个必要条件,那么, 未经同意,严禁以任何形式拷贝 35 第4章测试用例的设计 作为似乎更佳准则的判定覆盖的定义理应涵盖语句覆盖。
因此,判定覆盖要求每个判断都必须有“是”和“否”的结果,并且每条语句都至少被执行一次。
换一种更简单的表达方式,即每个判断都必须有“是”和“否”的结果,而且每个入口点(包括ON单元)都必须至少被调用一次。
我们的探讨仅针对有两个选择的判断或分支,当程序中包含有多重选择的判断时,判定/分支覆盖准则的定义就必须有所改变。
典型的例子有包含select(case)语句的Java程序,包含算术(三重选择)IF语句、计算或算术GOTO语句的FORTRAN程序,以及包含可选GOTO语句或GO-TO-DEFENDING-ON语句的COBOL程序。
对于这些程序,判定/分支覆盖准则将所有判断的每个可能结果都至少执行一次,以及将程序或子程序的每个入口点都至少执行一次。
在图4-1中,两个涵盖了路径ace和abd,或涵盖了路径acd和abe的测试用例就可以满足判定覆盖的要求。
如果我们选择了后一种情况,两个测试用例的输入是A=
3,B=
0,X=3和A=
2,B=
1,X=
1。
判定覆盖是一种比语句覆盖更强的准则,但仍然相当不足。
举例来说,我们仅有50%的可能性遍历到那条X未发生变化的路径(也即,仅当我们选择前一种情况)。
如果第二个判断存在错误(例如把X>1写成了X<
1,那么前面例子中的两个测试用例都无法找出这个错误。
比判定覆盖更强一些的准则是条件覆盖。
在条件覆盖情况下,要编写足够的测试用例以确保将一个判断中的每个条件的所有可能的结果至少执行一次。
因为,就如同判定覆盖的情况一样,这并不总是能让每条语句都执行到,因此作为对这条准则的补充就是对程序或子程序,包括ON单元的每一个入口点都至少调用一次。
举例来说,分支语句 DOK=0to50WHILE(J+K因此,需要针对K<=50、K>50(达到循环的最后一次迭代)以及J+K=QUEST的情况设计测试用例。
图4-1有四个条件:A>
1、B=
0、A=2以及X>
1。
因此需要足够的测试用例,使得在点a处出现A=
2、A<
2、X>1及X<=1的情况。
有足够数量的测试用例满足此准 未经同意,严禁以任何形式拷贝 36 软件测试的艺术 则,用例及其遍历的路径如下所示:
1.A=
2,B=
0,X=4ace2.A=
1,B=
1,X=1adb 请注意,尽管在本例中生成的测试用例数量是一样的,但条件覆盖通常还是要比判定覆盖更强一些。
因为,条件覆盖可能(但并不总是这样)会使判断中的各个条件都取到两个结果(“真”和“假”),而判定覆盖却做不到这一点。
举例来说,在相同的分支语句 DOK=0to50WHILE(J+K如果使用的是判定覆盖测试,将循环从K=0执行到K=51即可满足该准则,但从未考虑到WHILE子句为假的情况。
如果使用的是条件覆盖准则,就需要设计一个测试用例为J+K 虽然条件覆盖准则乍看上去似乎满足判定覆盖准则,但并不总是如此。
如果正在测试判断条件IF(A&B),条件覆盖准则将要求编写两个测试用例:A为真,B为假;A为假,B为真。
但是这并不能使IF语句中的THEN被执行到。
对图4-1所示例子所进行的条件覆盖测试涵盖了全部判断结果,但这仅仅是偶然情况。
举例来说,两个可选的测试用例:
1.A=
2,B=
0,X=32.A=
1,B=
1,X=
1 涵盖了全部的条件结果,却仅涵盖了四个判断结果中的两个(这两个测试用例都涵盖到了路径abe,因而不会执行第一个判断结果为真的路径,以及第二个判断结果为假的路径)。
显然,解决上面左右为难局面的办法就是所谓的判定/条件覆盖准则。
这种准则要求设计出充足的测试用例。
将一个判断中的每个条件的所有可能的结果至少执行一次,将每个判断的每个条件的所有可能的结果至少执行一次,将每个判断的所有可能的结果至少执行一次,将每个入口点都至少调用一次。
判定/条件覆盖准则的一个缺点是尽管看上去所有条件的所有结果似乎都执行 未经同意,严禁以任何形式拷贝 37 第4章测试用例的设计 到了,但由于有些特定的条件会屏蔽掉其他的条件,常常并不能全部都执行到。
请参见图4-2来观察此种情况。
图4-2中的流程图描述的是编译器将图4-l中的程序编译生成机器代码的过程。
源程序中的多重条件判断被分解成单个的判断和分支,因为大多数的机器都没有能执行多重条件判断的单独指令。
那么,更为完全的测试覆盖似乎是将每个基本判断的全部可能的结果都执行到,而前两个判定覆盖的测试用例都做不到这点,它们未能执行到判断H中的结果为“假”的分支,以及判断K中结果为“真”的分支。
HA>1YN JA=2YN KX>1YNd B=0N IY X=X/A X=X+
1 图4-2图4-1中程序的机器码 如图4-2所示,其中的原因是“与”和“或”表达式中某些条件的结果可能会屏蔽掉或阻碍其他条件,的判断。
举例来说,如果“与”表达式中有个条件为“假”,那么就无须计算该表达式中的后续条件。
同样,如果“或”表达式中有个条件为“真”,那么后续条件也无须计算。
因此,条件覆盖或判定/条件履盖谁则不一定会发现逻辑表达式中的错误。
所谓的多重条件覆盖准则能够部分解决这个问题。
该准则要求编写足够多的测 未经同意,严禁以任何形式拷贝 38 软件测试的艺术 试用例,将每个判定中的所有可能的条件结果的组合,以及所有的入口点都至少执行一次。
举例来说,考虑下面的伪代码程序; NOTFOUND=TRUE;DOI=1TOTABSIZEWHILE(NOTFOUND);/*SEARCHTABLE*/……searchinglogic……;END 要测试四种情况:
1.I<=TABSIZE,并且NOTFOUND为真;
2.I<=TABSIZE,并且NOTFOUND为假(在到达表格尾部前查询指定条目);
3.I>TABSIZE,并且NOTFOUND为真(查询了整个表格,未找到指定条目);
4.I>TABSIZE,并且NOTFOUND为假(指定条目位于表格的最后位置)。
很容易发现,满足多重条件覆盖准则的测试用例集,同样满足判定覆盖准则、条件覆盖准则以及判定/条件覆盖淮则。
再次回到图4-l中,测试用例必须覆盖以下8种组合:
1.A>
1,B=02.A>
1,B<>03.A<=
1,B<>04.A=
2,X>15.A=
2,X<=16.A<>
2,X>17.A<>
2,X<=
1 注意,与左边的情况一样,第5至第8组合表示了第二个if语句的值。
由于
X 可能在该if语句之前发生了改变,因此这个if语所需的值必需对程序逻辑进行回 溯,以找到相对应的输入值,要测试的这8种组合并不一定意味着需要设计出8个 测试用例。
实际上,用4个测试用例就可以覆盖它们。
下面是这些测试用例的输入, 以及它们覆盖的组合: A=
3,B=
0,X=4A=
2,B=
1,X=1A=
1,B=
0,X=2A=
1,B=
1,X=
1 覆盖组合1,5覆盖组合2,6覆盖组合3,7覆盖组合4,
8 未经同意,严禁以任何形式拷贝 39 第4章测试用例的设计 图4-1的程序存在4条不同的路径,需要4个测试用例,这样的情况纯属巧合。
事实上,这4个用例也没有覆盖到每条路径,路径acd就被遗漏掉了。
举例来说,对于如下所示的判断语句,尽管它只包舍两条路径,仍可能需要8个测试用例: if(x==y&&length(z)==0&&FLAG){j=1 elsei=1; } 在存在循环的情况下,多重条件覆盖准则所需要的测试用例的数量通常会远远小于其路径的数量。
总的来说,对于包含每个判断只存在一种条件的程序,最简单的测试准则就是设计出足够数量的测试用例,实现:
(1)将每个判断的所有结果都至少执行一次;
(2)将所有的程序入口(例如入口点或ON单元)都至少调用一次,以确保全部的语句都至少执行一次。
而对于包含多重条件判断的程序,最简单的测试准则是设计出足够数量的测试用例,将每个判断的所有可能的条件结果的组合,以及所有的入口点都至少执行一次(加入“可能”二字,是因为有些组合情况难以生成)。
4.1.2等价划分(EquivalencePartitioning) 本书第2章将一个好的测试用例描述为具有相当高的可能性发现某个错误来,此外还讨论了对程序的穷举输入测试是无法实现的。
因此,当测试某个程序时,我们就被限制在从所有可能的输入中努力找出某个小的子集。
理所当然,我们要找的子集必须是正确的,并且是可能发现最多错误的子集。
确定这个子集的一种方法,就是要意识到一个精心挑选的测试用例还应具备另外两个特性:
1.严格控制测试用例的增加,减少为达到“合理测试”的某些既定日标而必须设计的其他测试用例的数量。

2.它覆盖了大部分其他可能的测试用例。
也就是说,它会告诉我们,使用或不使用这个特定的输入集合,哪些错误会被发现,哪些会被遗漏掉。
虽然这两个特性看起来很相似,但描述的却是截然不同的两种思想。
第一个特 未经同意,严禁以任何形式拷贝 40 软件测试的艺术 性意味着,每个测试用例都必须体现尽可能多的不同的输入情况,以使最大限度地减少测试所需的全部用例的数量。
而第二个特性意味着应该尽量将程序输入范围进行划分,将其划分为有限数量的等价类,这样就可以合理地假设(但是,显然不能绝对肯定)测试每个等价类的代表性数据等同于测试该类的其他任何数据。
也就是说,如果等价类的某个测试用例发现了某个错误,该等价类的其他用例也应该能发现同样的错误。
相反,如果测试用例没能发现错误,那么我们可以预计,该等价类中的其他测试用例不会出现在其他等价类中,因为等价类是相互交迭的。
这两种思想形成了称为等价划分的黑盒测试方法。
第二种思想可以用来设计一个“令人感兴趣的”输入条件集合以供测试,而第一个思想可以随后用来设计涵盖这些状态的一个最小测试用例集。
本书第l章中三角形程序的一个等价类的例子是集合“三个值相等、都大于0的整型数据”。
将此作为一个等价类后,我们就可以说,如果对该集合中某个元素所进行的测试没有发现错误的话,那么对该集合中其他元素所进行的测试也不大可能会发现错误。
换言之,我们的测试时间最好花在其他地方(其他的等价类)。
使用等价划分方法设计测试用例主要有两个步骤:
(1)确定等价类;
(2)生成测试用例。
外部条件 有效等价类 无效等价类 图4-3等价类列举表
1.确定等价类 确定等价类是选取每一个输入条件(通常是规格说明中的一个句子或短语)并将其划分为两个或更多的组。
可以使用图4-3中的表格来进行划分。
注意,我们确定了两类等价类:有效等价类代表对程序的有效输入,而无效等价类代表的则是其他任何可能的输入条件(即不正确的输入值)。
这样,我们遵循了本书第2章阐述 未经同意,严禁以任何形式拷贝 41 第4章测试用例的设计 的测试原则,即要注意无效和未预料到的输入情况。
在给定了输入或外部条件之后,确定等价类大体上是一个启发式的过程。
下面给出了一些指导原则:
1.如果输入条件规定了一个取值范围(例如,“数量可以是从1到999”),那么就应确定出一个有效等价类(1<数量<999),以及两个无效等价类(数量<
1,数量>999)。

2.如果输入条件规定了取值的个数(例如,“汽车可登记一至六名车主”),那么就应确定出一个有效等价类和两个无效等价类(没有车主,或车主多于六个)。

3.如果输入条件规定了一个输入值的集合,而且有理由认为程序会对每个值进行不同处理(例如,“交通工具的类型必须是公共汽车、卡车、出租车、火车或摩托车”),那么就应为每个输入值确定一个有效等价类和一个无效等价类(例如,“拖车”)。

4.如果存在输入条件规定了“必须是”的情况,例如“标识符的第一个字符必须是字母”,那么就应确定一个有效等价类(首字符是字母)和一个无效等价类(首字符不是字母)。
如果有任何理由可以认为程序并未等同地处理等价类中的元素,那么应该将这个等价类再划分为小一些的等价类,稍后我们将给出这个过程的例子。

2.生成测试用例 第二步是使用等价类来生成测试用例,其过程如下:
1.为每个等价类设置一个不同的编号。

2.编写新的测试用例,尽可能多地覆盖那些尚未被涵盖的有效等价类,直到 所有的有效等价类都被测试用例所覆盖(包含进去)。

3.编写新的用例,覆盖一个且仅一个尚未被覆盖的无效等价类,直到所有的 无效等价类都被测试用例所覆盖。
用单个测试用例覆盖无效等价类,是因为某些特定的输入错误检查可能会屏蔽或取代其他输入错误检查。
举例来说,如果规格说明规定了“请输入书籍类型(硬皮、软皮或活页)及数量(l~999)”,代表两个错误输入(书籍类型错误,数量 未经同意,严禁以任何形式拷贝 42 软件测试的艺术 错误)的测试用例“XYZ0”,很可能不会执行对数量的检查,因为程序也许会提示“XYZ是未知的书籍类型”,就不检查输入的其余部分了。
4.1.3一个范例 作为一个例子,假设我们正在为FORTRAN语言的一个子集开发编译器,我们希望对DIMENSION语句的语法检查进行测试。
该语句的规格说明如下所示(这不是FORTRAN语言中的完整DIMENSION语句,我们对其进行了适当的剪裁,使其适合作为教科书的样例。
不要被其误导,以为测试实际的程序就像测试本书中的样例一样容易)。
在规格说明中,斜体字中的项是在实际语句中必须被特定实体取代的语法单元,使用括弧代表可选项,省略号代表前面的项可能会连续重复出现多次。
DIMENSION语句用来定义数组的大小 DIMENSION语句的格式如下: DIMENSIONad[,ad]… 其中ad是数组描述符,其格式如下: n(d[,d]…) 其中n是数组的符号名,d是数组的维说明符。
符号名可以由1~6个字母或数字组成,其中首字符必须是字母。
一个数组最少有1个维,最多有7个维。
维说明符的格式如下: [lb:]ub其中lb与ub分别是维的下边界和上边界。
边界可以是-65534~65535之间的一个常数,或是一个整型变变量名(但不能走数组元素名)。
如果未指定lb,则其默认值为
1。
ub的值必须大于或等于lb。
如果指定了lb,则其值可为负数、零或正数。
就全部语句而言,DIMENSION语句可写成连续多行(规格说明结束)。
第一步应该是确定输入条件,然后为输入条件确定等价类。
这些步骤都以表格 形式记录在表4-l中。
括号中的数字代表不同等价类的标识符。
表4-1等价类InputCondition NumberofarraydescriptorsSizeofarraynameArrayname Arraynamestartswithletter Valid Equivalence Classes one
(1),>one
(2) 1–6
(4) hasletters
(7),hasdigits
(8) yes(10) InvalidEquivalenceClassesnone(3)0
(5),>6(6)hassomethingelse
(9) no(11) 未经同意,严禁以任何形式拷贝 43 第4章测试用例的设计 NumberofdimensionsUpperboundis Integervariablename IntegervariablestartswithletterConstant LowerboundspecifiedUpperboundtolowerbound Specifiedlowerbound Lowerboundis Multiplelines 1–7(12)constant(15),integervariable(16)hasletter(19),digits(20) yes(22) -65534–65535(24) yes(27),greaterthan(29),equal(30) negative(32),zero(33),>0(34)constant(35),integervariable(36),yes(39), 0(13),>7(14)arrayelementname(17),somethingelse(18)hashassomethingelse(21)no(23) ≤65534(25),(26)no(28)lessthan(31) >65535 arrayelementname(37)somethingelse(38)no(40) 下一个步骤应该是编写一个测试用例以覆盖一个或多个有效等价类。
举例来说, 测试用例DIMENSIONA
(2) 覆盖了第1,4,7,10,12,15,24,28,29,40等价类。
再下一个步骤应该是设计一个或更多的测试用例以覆盖剩余的有效等价类,如 以下形式的测试用例 DIMENSIONA12345(1,
9,J4XXXX,65535,
1,KLM,X100),BBB[-65534:100,0:1000,10:10,I:65535] 覆盖了剩余的等价类。
而无效输入等价类及其测试用例如下所示:
(3):DIMENSION
(5):DIMENSION(10)
(6):DIMENSIONA234567
(2)(9):DIMENSIONA.1
(2)(11):DIMENSION1A(10)(13):DIMENSIONB(14):DIMENSIONB(4,4,4,4,4,4,4,4)(17):DIMENSIONB(
4,A
(2))(18):DIMENSIONB(4,7)(21):DIMENSIONC(1,10)(23):DIMENSIONC(10,1J)(25):DIMENSIONC(-65535:1)(26):DIMENSIONC(65536)(31):DIMENSIOND(4:3)(37):DIMENSIOND(A
(2):4) 未经同意,严禁以任何形式拷贝 44 软件测试的艺术 (38):D(.:4)因此,所有的等价类都被18个测试用例全部所覆盖了。
读者可以考虑一下,如何将这些测试用例与用特殊方法生成的测试用例集进行比较。
尽管等价划分方法要比随机选取测试用例优越得多,但它仍然存在不足。
例如,这种方法忽略了某些特定类型的高效测试用例,下面介绍的两种方法(边界值分析与因果图)可以弥补其中的很多不足。
4.1.4边界值分析(Boundary-ValueAnalysis) 经验证明,考虑了边界条件的测试用例与其他没有考虑边界条件的测试用例相比,具有更高的测试回报率。
所谓边界条件,是指输入和输出等价类中那些恰好处于边界、或超过边界、或在边界以下的状态。
边界值分析方法与等价划分方法存在两方面的不同:
1.与从等价类中挑选出任意一个元素作为代表不同,边界值分析需要选择一个或多个元素,以便等价类的每个边界都经过一次测试。

2.与仅仅关注输入条件(输入空间)不同,还需要考虑从结果空间(输出等价类)设计测试用例。
很难提供一份如何进行边界值分析的“详细说明’,因为这种方法需要一定程度的创造性,以及对问题采取一定程度的特殊处理办法(因此,就像测试的许多其他方面一样,这更多的是项智力工作,并非其他的什么)。
然而,我们还是给读者提供一些通用指南:
1.如果输入条件规定了一个输入值范围,那么应针对范围的边界设计测试用例, 针对刚刚越界的情况设计无效输入测试用例。
举例来说,如果输入值的有效范围是-1.0至+l.0,那么应针对-1.0、1.0、-1.001和1.001的情况设计测试用例。

2.如果输入条件规定了输入值的数量,那么应针对最小数量输入值、最大数量输入值,以及比最小数量少一个、比最大数量多一个的情况设计测试用例。
举例来说,如果某个输入文件可容纳l~255条记录,那么应根据0、l、255和256条记录的情况设计测试用例。
未经同意,严禁以任何形式拷贝 45 第4章测试用例的设计
3.对每个输出条件应用指南
1。
举例来说,如果某个程序按月计算FICA1的扣除额,且最小金额是$0.00,最大金额为$1165.25,那么应该设计测试用例来测试扣除$0.00和$1165.25的情况。
此外,还应观察是否可能设计出导致扣除金额为负数或超过$1165.25的测试用例。
注意,检查结果空间的边界很重要,因为输入范围的边界并不总是能代表输出范围的边界情况(例如,三角正弦函数sin的情况就如此)。
同样,总是产生超过输出范围的结果也是不大可能的,但无论如何,应该考虑这种可能性。

4.对每个输出条件应用指南
2。
如果某个信息检索系统根据输入请求显示关联程度最高的信息摘要,而摘要的数量从未超过4条,则应编写测试用例,使程序显示0条、l条和4条摘要,还应设计测试用例,导致程序错误地显示5条摘要。

5.如果程序的输入或输出是一个有序序列(例如顺序的文件、线性列表或表格),则应特别注意该序列的第一个和最后一个元素。

6.此外,发挥聪明才智找出其他的边界条件。
本书第1章中的三角形分析程序可以说明边界值分析的必要性。
作为代表三角形的输入值,它们必须是大于0的整数,而且其中任意两个之和应大于第三个。
如果定义了等价划分,可能会确定一个满足此条件的等价类,以及另一个两个输入之和不大于第三个的等价类。
因此,3-4-5和1-2-4两个都是可能的测试用例。
然而,我们遗漏了一个可能的错误,即如果程序中表达式写成了A+B>=
C,而不是A+B>
C,那么程序就会错误地告诉我们l-2-3表示的是一个有效的不规则三角形。
因此,边界值分析方法和等价划分之间的重要区别是,边界值分析考察正处于等价划分边界或在边界附近的状态。
作为边界值分析的一个例子,考虑下面的程序规格说明: MTEST是一个多项选择考试的评分程序。
程序的输入是一个名为OCR的数据文件,包含多个长度为80个字符的记录。
按照文件的格式要求.第一个记录的内容是标题,作为每份输出报告的标题。
后面的一组记录描述了试题的标准答案,这些记录的最后一个字符是“2”。
在这组记录的首条记录中,第l~第3列存储的是试题的数量(一个l~999的数),第10~第59存储的是第l~第 1美国联邦社会保险捐款法.纳税人应依据此项法律交纳一定金额。
-译者注 未经同意,严禁以任何形式拷贝 46 软件测试的艺术 50道试题的标准答案(任何字符都为有效答案),后续记录的第10~第59列存储的是第51~第100道试题、第101~第150道试题的标准答案等等。
第三组记录描述的是每个学生的答案,这些记录的最后一个字符皆为“3”。
对于每个学生来说,第一条记录的第1~第9列存储的是学生的名字或编号(任意字符),第10~第59列存储的是该学生对第l~第50道试题的答案。
如果本次考试试题超过50个,该学生的后续记录的第10~第59列存储的是第51~第100、第101~第150道试题的答案等等。
学生的人数最多是200。
输入数据如图4-4所示。
四个输出报告分别是:
1.按学生的编号排序的报告,显示每名学生的成绩(正确答案的百分比)和名次。

2.按成绩排序的报告。

3.显示成绩的平均值、中间值和标准偏差的报告。

4.按问题的编号排序的报告,显示正确回答每个问题的学生比例。
(规格说明结束) 未经同意,严禁以任何形式拷贝 47 第4章测试用例的设计
1 试题数量 134
1 标题 80 第1~50试题的答案 910 5960 2 7980 第51~100试题的答案 910 5960 2 7980 学生标识符 第1~50试题的答案
1 910 5960 3 7980 第51~100试题的答案
3 1 910 59607980 学生标识符 第1~50试题的答案
1 910 5960 3 7980 图4-4MTEST程序的输入 我们从仔细阅读规格说明开始,寻找输入条件。
第一个边界输入条件是一个空输入文件。
第二个输入条件是该标题记录,边界条件是标题记录不存在、可能的最短标题和最长标题。
后面的输入条件是存储标准答案的记录,以及第一个标准答案记录里的“试题数量”域是否存在。
试题数量的等价类不应是l~999,因为在每个50的倍数处会出现某些特殊情况(例如,需要多个记录)。
这种输入等价类的一个合理划分是1~50、51~999。
因此,我们需要针对试题数量为0、l、50、51和999的情况设计测试用例。
这样就覆盖了标准答案记录数量的大多数边界条件。
然而,三个最令人感兴趣的输入条件是标准答案记录不存在、记录多了一个以及记录少了一个(例如,试题数量是60个,然而在某个情况下有三个标准答案记录,而在另一种情况下只有一个)。
到目前为止,我们生成的测试用例有:
1.输入文件为空。

2.没有标题记录。
未经同意,严禁以任何形式拷贝 48 软件测试的艺术
3.标题只有1个字符。

4.标题有80个字符。

5.考试试题数量为
1。

6.考试试题数量为50。

7.考试试题数量为51。

8.考试试题数量为999。

9.考试试题数量为
0。
10.试题数量域的值为非数字类型。
11.标题记录后无标准答案记录。
12.标准答案记录数量多一个。
13.标准答案记录数量少一个。
下面的输入条件是有关学生的答案的,其边界值测试用例可以是:14.学生人数为
0。
15.学生人数为
1。
16.学生人数为200。
17.学生人数为201。
18.某个学生只有一条答案记录,但却存在两条标准答案记录。
19.上面那个学生是文件中第一个学生。
20.上面那个学生是文件中的最后一个学生。
21.某个学生有两条答案记录,但只有一条标准答案记录。
22.上面那个学生是文件中第一个学生。
23.上面那个学生是文件中最后一个学生。
尽管有些输出边界(例如第一份输出报告为空)已被已有的测试用例覆盖到,但我们仍然可以通过检查输出边界而得到有用的测试用例集。
第一份输出报告与第二份输出报告的边界条件是: 学生人数为0(同第14号测试样例)。
学生人数为1(同第15号测试样例)。
学生人数为200(同第16号测试样例)。
24.所有学生的成绩相同。
未经同意,严禁以任何形式拷贝 49 第4章测试用例的设计 25.所有学生的成绩都不相同。
26.部分、但不是全部学生的成绩相间(检查名次的计算是否正确)。
27.某个学生的成绩为
0。
28.某个学生的成绩为100。
29.某个学生的标识符值为可能的最低值(检查排序)。
30.某个学生的标识符值为可能的最高值。
31.学生的数量恰好够一份报告占满一页(检查是否打印出多余页)。
32.学生的数量除够一份报告占满一页外,还多一个。
第三份输出报告(平均值、中间值和标准偏差)的边界条件是;33.平均值为其最大值(全部学生都得满分)。
34.平均值为0(全部学生都得0分)。
35.标准偏差为其最大值(一个学生成绩为0分,其他都为100分)。
36.标准偏差为0(全部学生成绩相同)。
第33和第34号测试用例同时也覆盖了中间值的边界条件。
另外一个有用的测试用例是学生人数为0的情况(检查程序在计算平均值时是否有被0除的情况),只是这种情况与第14号测试用例相同。
对第四份输出报告的检查可以生成下列边界值测试用例:37.全部学生都回答正确第一道试题。
38.全部学生都回答错误第一道试题。
39.全部学生都回答正确最后一道试题。
40.全部学生都回答错误最后一道试题。
41.试题的数量恰好够一份报告占满一页.42.试题的数量除够一份报告占满一页外,还多一道。
有经验的程序员很可能会认同这一点,即42个测试用例的大部分代表了在开发该程序的过程中可能会犯的共性错误,但如果我们采用的是随机生成或特殊的测试用例设计方法,这些错误中的大多数都不会被检查出来。
如果使用得正确,边界值分析是最为有效的测试用例设计方法之
一。
然而,这种方法常常使用得不好,因为表面上它听起来比较简单。
我们应该认识到,边界条件可能非常微妙,因此把它们确定下来需要煞费一番脑筋。
未经同意,严禁以任何形式拷贝 50 软件测试的艺术 4.1.5因果图(Cause-EffectGraphing) 边界值分析和等价划分的一个弱点是未对输入条件的组合进行分析。
举例来说,上一节中介绍的MTEST程序由于试题数量与学生数量的乘积超过某个阈值时可能会发生失效(例如,程序耗尽了内存)。
边界值测试不一定能检查出此类错误。
对输入组合进行测试并不是简单的事情,因为即使对输入条件进行了等价划分,这些组合的数量也是个天文数字。
如果在选择输入条件的子集时没有采用一个系统的方法,很可能选择出一个任意的输入条件子集,这样会使测试没有什么成效。
因果图有助于用一个系统的方法选择出高效的测试用例集。
它还有一个额外的好处,就是可以指出规格说明的不完整性和不明确之处。
因果图是一种形式语言,用自然语言描述的规格说明可以转换为因果图。
因果图实际上是一种数字逻辑电路(一个组合的逻辑网络)、但没有使用标准的电子学符号,而是使用了稍微简单点的符号。
除了了解布尔逻辑(了解逻辑运算符“与”、“或”、“非”)之外,读者不必掌握电子学方面的知识。
生成测试用例时采用的过程如下:
1.将规格说明分解为可执行的片段。
这是必须的步骤,因为因果图不善于处 理较大的规格说明。
举例来说,当测试一个电子商务系统时,“可执行的片段”可能是指对挑选和确认购物车中的单件商品的规格说明。
在测试一个Web页面设计时,我们可能会测试一个单独的菜单树,甚至是一个不太复杂的导航序列。

2.确定规格说明中的因果关系。
所谓“因”,是指一个明确的输入条件或输入条件的等价类。
所谓“果”,是指一个输出条件或系统转换(输入对程序或系统状态的延续影响)。
举例来说,如果某个事务引起文件或数据库记录被修改,那么这种改变就是一个系统转换,而系统反馈的确认信息就是一个输出条件。
通过逐字逐句地阅读规格说明,同时标识出描述“因”和“果”的文字或句子,就可以将“因”和“果”确定出来。
因果关系一旦确定下来,每个“因”和“果”都被赋予一个惟一的编号。

3.分析规格说明的语义内容,并将其转换为连接因果关系的布尔图。
这就是 未经同意,严禁以任何形式拷贝 51 第4章测试用例的设计 所谓的因果图。

4.给图加上注解符号,说明由于语法或环境的限制而不能联系起来的“因” 和“果”。

5.通过仔细地跟踪图中的状态变化情况,将因果图转换成一个有限项的判定 表。
表中的每一列代表一个测试用例。

6.将判定表中的列转换成测试用例。
因果图中的基本符号如图4-5所示。
设想一下,每个结点的值为0或为1,0代表“不存在”状态,1代表“存在”状态。
identity函数表示如果a等于
1,则b也为
1,否则b为
0。
not函数表示如果a等于
1,则b为
0,否则b为
1。
or函数表示如果a或b或c等于
1,则d为
1,否则d为
0。
and函数表示如果a和b都等于
1,则c为
1,否则c为
0。
后两个函数(or和and)允许存在任意数量的输入。
图4-5基本的因果图符号 为描述一个小的因果图,考虑下面的规格说明: 第一列中的字符必须是“A”或“B”,第二列中的字符必须是一个数字。
在这种情况下,对文件进行更新。
如果第一个字符不正确,产生提示信息X12。
如果第二个字符不是数字,产生提示信息X13。
“因”如下: 未经同意,严禁以任何形式拷贝 52 软件测试的艺术 1——第一列的字符是“A”2——第一列的字符是“B”3——第二列的字符是一个数字 “果”如下:70——对文件做了更新71——产生提示信息X1272——产生提示信息X13 因果图如图4-6所示。
请注意生成的中间结点11。
通过设置“因”的全部可能状态,观察“果”得到了正确的值,就可以确认该图代表了规格说明。
对于熟悉逻辑图的读者来说,图4-7是一个等价的逻辑电路。
图4-6因果图范例 图4-7与图4-6等价的逻辑图 未经同意,严禁以任何形式拷贝 53 第4章测试用例的设计 尽管图4-6所示的因果图代表了规格说明,但图中包含了一个不可能的原因组合,即原因1和原因2不可能同时设置为
1。
在大多数程序中,由于语法或环境的原因,某些原因的组合是不可能存在的(一个字符不能同时为“A”和“B”)。
为了对此做出解释,我们采用图4-8所示的符号。
约束E表示其必须总为真,而a和b最多只有一个为1(a与b不能同时为1)。
约束I表示其为真时,a、b、c中至少有一个应为1(a、b、c不能同时为0)。
约束O表示a、b中有且仅有一个必须为
1,约束R表示如果a为1,b也必须为1(例如,a为1而b为0的情况是不可能的)。
图4-8约束符号 在结果之间通常需要建立约束关系。
图4-9中的约束M表示,如果结果a为
0,则b强制为
0。
未经同意,严禁以任何形式拷贝 54 软件测试的艺术 图4-9屏蔽约束的符号再回到前面那个简单例子中来,我们看到,原因1和原因2实际上是不可能同时成立的,而两者都不成立却是可能的。
因此,它们之间应该用约束E来连接,如图4-10所示。
图4-10带有“排斥性”约束条件的因果图范例 为了说明如何从因果图中导出测试用例,需要使用下面介绍的规格说明。
该规格说明用于某个交互系统的一条调试命令。
DISPLAY命令用于从一个终端窗口中观察内存空间的内容。
该命令的语法见图4-11。
括弧表示可替换的可选操作时象。
大写字母表示操作对象的关健字;小写字母表示操作时象的值(即要被取代的实际值)。
带下划线的操作对象代表默认值(即操作对象默认时所使用的值)。
第一个操作对象(hexloc1)规定了待显示的内容的首字节地址。
该地址可以是1~6位长度的十六进制(0~
9,A~F)数。
如果地址没有指定,默认地址为
0。
地址必须在机器实际内存地址的范围之内。
第二个操作对象规定了要显示的内存的数量。
如果规定了hexloc2的值,也就确定了要显示的内存空间范围内的末字节地址。
该地址可以是1~6位长度的十六进制数,且必须大于或等于起始地址(hexloc1)。
同时,hexloc2也必须在机器实际内存地址的范围之内。
如果定义了END,那么从起始位置(hexloc1)直到机器内存中最后字节的内容都将显示出来。
如果规定了bytecount的值,也就规定了要显示的内存字节数量(从hexloc1指定的位置开始计算),该操作对象是 未经同意,严禁以任何形式拷贝 55 第4章测试用例的设计 一个十六进制整数(长度为l~6位)。
bytecount与hexloc1之和不能超过实际的内存容量加
1,而bytecount的值至少为
1。
当显示内存内容时,在屏幕上按如下格式分一行或多行输出:xxxxxx=word1word2word3word4xxxxxx是以十六进制表示的word1的地址。
无论hexloc1为何值,或者要显示的内存容量是多大,总是显示整数个字(一个字由4个字节排列组成,字中首字节的地址是4的倍数)。
每一输出行总是包含4个字(16个字节)。
被显示范围的首字节包含在第一个字中。
(规格说明结束) 可能产生的错误信息如下:M1无效的命令语法。
M2所需内存超出了实际的内存范围。
M3所需内存为0或为负数。
图4-11DISPLAY命令的语法 例如: DISPLAY显示内存的前四个字(默认的起始位置为
0,默认的字节数为1)。
DISPLAY77F显示首字节地址为77F的字以及后续的3个字。
DISPLAY77F-407A显示从字节77F-字节407A间的字。
未经同意,严禁以任何形式拷贝 56 软件测试的艺术 DISPLAY77F.6显示从字节77F起六个字节的字。
DISPLAY50FF-END显示从字节50FF开始直到内存结束的字。
第一步骤是认真地分析规格说明以确定出“因”和“果”。
“因”如下:
1.存在第一个操作对象。
2.hexloc1操作对象仅包含十六进制数字。
3.hexlocl操作对象包含1-6个字符。
4.hexloc1操作对象在机器实际的内存范围之内。

5.第二个操作对象为END。

6.第二个操作对象为hexloc2。

7.第二个操作对象为bytecount。

8.第二个操作对象默认。
9.hexloc2操作对象仅包含十六进制数字。
10.hexloc2操作对象包含l-6个字符。
11.hexloc2操作对象在机器实际的内存范围之内。
12.hexloc2操作对象大于或等于hexloc1操作对象。
13.bytecount操作对象仅包含十六进制数字。
14.bytecount操作对象包含1-6个字符。
15.bytecount+hexloc1<=内存容量+
1。
16.bytecount>=
1。
17.定义的内存范围大到足够要求显示多行输出。
18.显示内存的起始位置不在字单元的边界位置。
每个“因”都按不同的数字进行了编号。
注意,其中四个“因”(第5~第8)是第二个操作对象所必需的,因为第二个操作对象可能是:
(1)END;(2)hexloc2;(3)bytecount;
(4)不存在;
(5)以上情况都不是。
“果”如下: 91.显示了信息M1。
92.显示了信息M2。
未经同意,严禁以任何形式拷贝 57 第4章测试用例的设计 93.显示了信息M3。
94.内存内容显示在一行上。
95.内存内容显示在多行上。
96.显示范围的首字节正好在字单元的边界位置。
97.显示范围的首字节不在字单元的边界位置。
下一个步骤是建立因果图。
“因”结点垂直排列在纸的左边,“果”结点垂直排列在纸的右边。
应仔细分析规格说明所表达的意思,以便建立起“因”与“果”的连接关系(即说明在何种情况下产生何种结果)。
图4-12显示了因果图的最初形式。
中间结点32表示一个语法上有效的第一个操作对象;结点35表示一个语法上有效的第二个操作对象;结点36表示一条语法上有效的命令。
如果结点36为
1,结果91(错误信息)就不会显示出来,反之就会显示。
未经同意,严禁以任何形式拷贝 58 软件测试的艺术 图4-12DISPLAY命令的初样图 完整的因果图如图4-13所示。
应当仔细地研究该图,以确认它如实地反映了规格说明的内容。
未经同意,严禁以任何形式拷贝 59 第4章测试用例的设计 图4-13不带约束条件的完整因果图 如果我们利用图4-13来生成测试用例,就会生成很多不可能实现的测试用例出来。
原因是由于语法的制约,有些特定的原因组合是不可能的。
举例来说,除非出现了原因l,原因2和原因3就不可能出现。
而原因4只有当原因2和原因3都出现时才成立。
图4-14显示的就是带有约束条件的完整的因果图。
请注意,原因5、6、7、8中最多仅能出现一个。
其余所有的原因约束条件都是“要求”关系(require,即图4-8中的约束
R。
--译者注)。
注意原因17(多行输出)要求原因8(第二个操作对象默认)不成立;仅当原因8不出现时,原因17方才出现。
再一次提醒,应 未经同意,严禁以任何形式拷贝 60 该仔细地检查这些约束条件。
软件测试的艺术 图4-14DISPLAY命令的完整因果图 下一步骤是建立有限项的判定表。
读者如果熟悉判定表的话,就会知道“因”是条件,而“果”是动作。
采用的过程如下:
1.选择一个“果”作为当前状态
(1)

2.对因果图进行回溯,查找导致该“果”为1(根据约束条件)的所有“因” 的组合。

3.在判定表中为每个“因”的组合生成一列。

4.对于每种“因”的组合,判断所有其它“果”的状态,并放置在每一列中。
在执行第2步时,需要做以下考虑:
1.当回溯经过一个结果应为1的or结点时,不要同时将该or结点的一个以 上的输入设置为
1。
这就是所谓的路径敏感性(pathsensitizing),其目的是 未经同意,严禁以任何形式拷贝 61 第4章测试用例的设计 避免由于原因之间的屏蔽而漏掉某些错误。

2.当回溯经过一个结果应为0的and结点时,显然应列举出导致结果为0的 所有的输入组合情况。
然而,如果碰到的情况是一个输入为
0,其它的输入中有一个或更多为
1,那么就无须罗列出其它输入可能为1的所有情况。

3.当回溯经过一个结果应为0的and结点时,仅有一种所有输入皆为0的情况需要列举出来(如果这个and结点位于因果图的中部,其输入来自于其他中间结点,那么所有输入都为的情况就会非常多)。
这些复杂的思路在图4-15中总结出来,图4-16是一个范例。
情况 如果x取值为l,则无须考虑当a=b=l时的情况(第l种考虑) 如果x取值为
0,则列举当a=b=0时的所有情况 如果x取值为l,则无须考虑当a=b=c=l时的所有情况 如果x取值为
0,仅需考虑当a=b=c=0时的情况(第3种考虑)。
对于abc为001、010、100、011、101和110时,每种仅需考虑一种情况(第2种考虑) 图4-15追溯因果图的思路 假设我们需要找出所有导致输出状态为0的输入条件。
根据上述第3条思路,我们只需列出一种情况,即结点5和结点6都是0的情况。
根据第2条思路,对于结点5为1而结点6为0的情况,我们只需列出结点5为l的这一种情况,而不必罗列出结点5可能为l的所有情况。
同样地,对于结点5为0而结点6为l的情况,我们也只需列出结点6为1的这一种情况(尽管在本例中只有一种)。
根据第1条思路,当结点5应被设置为l时,我们不应将结点1和结点2同时设置为
1。
因此,举例来说,我们可以处于从结点1到结点4间的5种状态,而并非是从结点1到结点4间导致输出为0的13种可能的状态,其值如下: 未经同意,严禁以任何形式拷贝 62 00001000100110100011 软件测试的艺术 (5=0,6=0)(5=l,6=0)(5=1,6=0)(5=1,6=0)(5=0,6=1) 图4-16描述追溯思路的因果图范例 这些思路也许看起来反复无常,但都有一个重要的目标:即减少因果图中的组合关系。
它们排除了会导致生成低效测试用例的状态。
如果不能排除低效测试用例,那么一个因果关系复杂的大因果图会生成天文数字的测试用例。
如果测试用例的数量大得不切合实际,就只得从中挑出一些子集来,而这又不能保证低效的测试用例会被排除在外。
因此,最好在分析因果图的阶段就将其排除掉。
现在,可将图4-14所示的因果图转化为判定表。
最先应选择结果91。
当结点36为0时,才会出现结果91。
当结点32和35为0,0;0,l或l,0时,结点36才会为
0,此处可以应用第2条和第3条思路。
通过对原因的回溯以及对原因间约束关系的考虑,可以找出导致结果91出现的原因组合,尽管这样做是个颇费力气的过程。
针对结果91出现的情况,其判定表如图4-17所示(第1列~第11列)。
第l 列~第3列(也是1~3号测试用例)代表结点32为0而结点35为l的情况,而第 4列~第10列代表结点32为1而结点35为0的情况。
根据第3条思路,在结点 32和35皆为0的全部21种情况中只需确定一种(第11列)。
表格中的空白处表示 未经同意,严禁以任何形式拷贝 63 第4章测试用例的设计 “无关紧要”的情况(即与该原因的状态并不相关),或指出由于其他相依赖原因的关系,该原因的状态是显而易见的(例如在第l列中,我们知道由于与原因6存在“至多一个”的关系,原因5、7和8必定为0)。
图4-17生成的判定表的前半部分 第12列~第15列代表结果92出现了的情况。
第16列~第17列代表结果93出现了的情况。
图4-18描述了判定表的剩余部分。
未经同意,严禁以任何形式拷贝 64 软件测试的艺术 图4-18生成的判定表的后半部分 最后一个步骤,是将判定表转化为38个测试用例。
下面列举了一组38个测试 用例。
每个测试用例右边的数字指出期望出现的结果。
我们假定所用计算机内存的 最末地址是7FFF。
1DISPLAY234AF74-123 (91) 2DISPLAY2ZX4-3000 (91) 3DISPLAYHHHHHHHH-2000 (91) 4DISPLAY200200 (91) 5DISPLAY0-22222222 (91) 6DISPLAY1-2X (91) 7DISPLAY2-ABCDEFGHI (91) 8DISPLAY3.1111111 (91) 9DISPLAY44.$42 (91) 10DISPLAY100.$$$$$$$ (91) 11DISPLAY10000000-
M (91) 12DISPLAYFF-8000 (92) 未经同意,严禁以任何形式拷贝 65 第4章测试用例的设计 13DISPLAYFFF.700114DISPLAY8000-END15DISPLAY8000-800116DISPLAYAA-A917DISPLAY7000.018DISPLAY7FF9-END19DISPLAY120DISPLAY21-2921DISPLAY4021.A22DISPLAY–END23DISPLAY24DISPLAY–F25DISPLAY.E26DISPLAY7FF8-END27DISPLAY600028DISPLAYA0-A429DISPLAY20.830DISPLAY7001-END31DISPLAY5-1532DISPLAY4FF.10033DISPLAY–END34DISPLAY–2035DISPLAY1136DISPLAY7000-END37DISPLAY4-1438DISPLAY500.11 (92)92)(92)(93)(93)(94,97)(94,97)(94,97)(94,97)(94,96)(94,96)(94,96)(94,96)(94,96)(94,96)(94,96)(94,96)(95,97)(95,97)(95,97)(95,96)(95,96)(95,96)(95,96)(95,96)(95,96) 注意,在大多数情况下,如果对同样一组原因执行两个或更多的测试用例,为改进测试用例的效率,这些原因应取不同的值。
还应注意到,由于受到实际存储空间的限制,第22号测试用例是不能实现的(就如同第33号测试用例,其产生的结果是95而不是94)。
因此,最终确定了37个测试用例。
评语 因果图方法是一个根据条件的组合而生成测试用例的系统性的方法。
可以替代这种方法的是特殊选取的条件组合,但在这个过程中,很可能会遗漏很多可由因果图方法确定的“令人感兴趣的”测试用例。
由于因果图方法需要将规格说明转换为一个布尔逻辑网络,因此它使我们从不同的视角,以更多的洞察力来审视规格说明。
事实上,建立因果图是一个暴露规格 未经同意,严禁以任何形式拷贝 66 软件测试的艺术 说明中模糊和不完整之处的好方法。
举例来说,聪明的读者也许已经注意到,上文讨论的过程已经发现了DISPLAY命令规格说明中的一个问题。
该规格说明规定,所有的输出行都包含4个字。
然而,这并不是对所有的情况都成立;在测试用例18和26中就不会发生,因为其起始地址距内存最末位置不足16个字节。
尽管因果图方法确实能产生一组有效的测试用例,但通常它不能生成全部应该被确定的有效测试用例。
举例来说,在上面的例子中,我们并未提到验证显示出来的内存值是否与内存中的实际值一致,也未提到对程序能否显示出内存空间中任何可能的值进行判断。
另外,因果图方法没有充分考虑边界条件。
当然,在此过程中我们可以尝试覆盖边界状态。
例如,不将 hexloc2≥hexloc1 确定成一个“因”,而将其确定成两个“因”:hexloc2=hexloc1hexloc2>hexloc1 然而,这样做所带来的问题是使因果图急剧复杂化,导致生成的测试用例的数量非常庞大。
鉴于此,最好是单独考虑边界值分析。
举例来说,可以从DISPLAY命令规格说明中确定出下面的边界条件: 1.hexloc1为1位数字。
2.hexloc1为六位数字。
3.hexloc1为七位数字。
4.hexloc1=
0。
5.hexloc1=7FFF。
6.hexloc1=8000。
7.hexloc2为一位数字。
8.hexloc2为六位数字。
9.hexloc2为七位数字。
10.hexloc2=
0。
11.hexloc2=7FFF。
12.hexloc2=8000。
未经同意,严禁以任何形式拷贝 67 第4章测试用例的设计 13.hexloc2=hexloc1。
14.hexloc2=hexloc1+
1。
15.hexloc2=hexloc1−
1。
16.bytecount为一位数字。
17.bytecount为六位数字。
18.bytecount为七位数字。
19.bytecount=
1。
20.hexloc1+bytecount=8000。
21.hexloc1+bytecount=8001。
22.显示16个字节(一行)。
23.显示17个字节(两行)。
注意,这并不意味着需要编写出60(37+23)个测试用例来。
由于因果图方法给我们提供了选择操作对象具体值的灵活性,在由因果图生成测试用例时,可以将边界条件分析一并考虑进去。
在上面的例子中,通过对最初37个测试用例的一部分进行重新编写,可以覆盖所有的23种边界条件,而且不必增加任何测试用例。
因此,我们得到了一组虽然不多但却很有效的测试用例,并满足了两方面的目标。
注意,因果图方法是与本书第2章中的几个测试原则相一致的。
确定每个测试用例的预期输出是因果图方法的固有部分(判定表中的每一列指明了预期的结果)。
同时还应注意到,此方法鼓励我们查找未预料到的结果。
举例来说,第1列(也是第1号测试用例)指出预期应出现结果91,而不应出现结果92至结果97。
此方法中最具难度的部分是将因果图转化为判定表。
这个过程是有算法的,即意味着我们可以编写程序来自动完成这个过程。
已经有些商业软件可以帮我们完成这一转化。
4.2错误猜测(ErrorGuessing) 常常可以看到这种情况,有些人似乎天生就是测试的能手。
这些人没有用到任何特殊的方法(比如对因果图进行边界值分析),却似乎有着发现错误的诀窍。
对此的一个解释是这些人更多是在下意识中,实践着一种称为错误猜测的测试 未经同意,严禁以任何形式拷贝 68 软件测试的艺术 用例设计技术。
接到具体的程序之后,他们利用直觉和经验猜测出错的可能类型,然后编写测试用例来暴露这些错误。
由于错误猜测主要是一项依赖于直觉的非正规的过程,因此很难描述出这种方法的规程。
其基本思想是列举出可能犯的错误或错误易发情况的清单,然后依据清单来编写测试用例。
例如,程序的输入中出现0这个值就是一种错误易发情况。
因此,可以编写测试用例,检查特定的输入值中有
0,或特定的输出值被强制为0的情况。
同样,在出现输入或输出的数量不定的地方(如某个被搜索列表的条目数量)。
数量为“没有”和“一个”(例如空列表,仅包含一个条目的列表)也是错误易发情况。
另一个思想是,在阅读规格说明时联系程序员可能做的假设来确定测试用例(即规格说明中的一些内容会被忽略,要么是由于偶然因素,要么是程序员认为其显而易见)。
由于无法给出一个规程来,次优的选择是讨论错误猜测的实质,最好的做法是举出实例。
假设在测试一个排序程序,要探讨的情况如下: •输入列表为空。
•输入列表仅包含一个条目。
•输入列表所有条目的值都相同。
•输入列表已经排过序。
换言之,上面列举出的这些特殊情况可能在程序设计时被忽略。
如果要测试的是一个二进制搜索程序,需要检查的情况包括:(l)被搜索的表中只有一个条目;
(2)表的大小是2的幂(如16);
(3)表的大小是2的幂差1和2的幂多l(如15和17)。
想一想“边界值分析”一节中的MTEST程序。
当使用错误猜测方法之后,我们会想到以下增加的测试: •程序是否接受“空白”作为答案?•一个第2类型的记录(标准答案)出现在第3类型的记录集中(学生答案)。
•除了首条记录(标题)外,存在最后一列中没有“2”或“3”的记录。
•两位学生名字或编号相同。
•由于中间值的计算根据数据项的数量是奇数还是偶数而有所不同,因此针 未经同意,严禁以任何形式拷贝 69 第4章测试用例的设计 对学生数量为奇数和偶数的情况分别对程序进行测试。
•“问题数量”域的值为负数。
对前一节中的DISPLAY命令,所想到的错误猜测测试如下: •DISPLAY100- (第二个操作对象不全) •DISPLAY100. (第二个操作对象不全) •DISPLAY100-10A42(多余的操作对象) •DISPLAY000-0000FF(以0打头) 4.3测试策略 本章讨论的测试用例设计方法可以组合为一个整体的策略。
之所以组合,原因现在已经很清楚了,每一种方法都可以提供一组具体的有用的测试用例,但是都不能单独提供一个完整的测试用例集。
一组合理的策略如下:
1.如果规格说明中包含输入条件组合的情况,应首先使用因果图分析方法。

2.在任何情况下都应使用边界值分析方法。
应记住,这是对输入和输出边界 进行的分析。
边界值分析可以产生一系列补充的测试条件,但是,也正如“因果图分析”一节所述,多数甚至全部条件都可以被整合到因果图分析中。

3.应为输入和输出确定有效和无效等价类,在必要情况下对上面确认的测试用例进行补充。

4.使用错误猜测技术增加更多的测试用例。

5.针对上述测试用例集检查程序的逻辑结构。
应使用判定覆盖、条件覆盖、判定/条件覆盖或多重条件覆盖准则(最后的一个最为完整)。
如果覆盖准则未能被前四个步骤中确定的测试用例所满足,并且满足准则也并非不可能(由于程序的性质限制,某些条件的组合也许是不可能实现的),那么增加足够数量的测试用例,以使覆盖准则得到满足。
再一次声明,使用上述策略并不能保证可以发现所有的错误,但实践证明这是一个合理的折中方案。
同时,它也代表了客观的艰巨工作量,虽然没人说软件测试是一件容易的事。
未经同意,严禁以任何形式拷贝 70 软件测试的艺术 第5章模块(单元)测试 到目前为止,我们在很大程度上忽视了软件测试的机制及被测程序的规模。
然而,大型的软件程序(即超过500条语句块的程序)需要特别的测试对策。
在本章中我们将探讨构建大型程序测试的第一个步骤:模块测试,而剩余的步骤将在本书第6章中介绍。
模块测试(或单元测试)是对程序中的单个子程序、子程序或过程进行测试的过程,也就是说,一开始并不是对整个程序进行测试,而是首先将注意力集中在对构成程序的较小模块的测试上面。
这样做的动机有三个。
首先,由于模块测试的注意力一开始集中在程序的较小单元上,因此它是一种管理组合的测试元素的手段。
其次,模块测试减轻了调试(准确定位并纠正某个已知错误的过程)的难度,这是因为一旦某个错误被发现出来,我们就知道它在哪个具体的模块中。

三,模块测试通过为我们提供同时测试多个模块的可能,将并行工程引入软件测试中。
模块测试的目的是将模块的功能与定义模块的功能规格说明或接口规格说明进行比较。
为了再次强调所有测试过程的目的,这里的测试目标不是为了说明模块符合其规格说明,而是为了揭示出模块与其规格说明存在着矛盾。
在本章中,我们从以下三个方面来探讨模块测试:
1.测试用例的设计方式。

2.模块测试及集成的顺序。

3.对执行模块测试的建议。
5.1测试用例设计 在为模块测试设计测试用例时,需要使用两种类型的信息:模块的规格说明和模块的源代码。
规格说明一般都规定了模块的输入和输出参数以及模块的功能。
模块测试总体上是面向白盒测试的。
其中一个原因是如果对大一点的软件进行 测试,例如一个完整的程序(其实是后续的测试过程所针对的对象),白盒测试不 容易展开。
第二个原因是,后续的测试过程着眼于发现其他类型的错误(举例来说, 未经同意,严禁以任何形式拷贝 71 第5章模块(单元)测试 这些错误不一定与程序的逻辑结构有关,比如程序未能满足其用户需求)。
因此,模块测试的测试用例的设计过程如下:使用一种或多种白盒测试方法分析模块的逻辑结构,然后使用黑盒测试方法对照模块的规格说明以补充测试用例。
由于所需的测试用例设计方法已经在本书第4章中讨论过,我们在这里通过一个例子来描述这些方法在模块测试中的应用。
假设我们要测试一个名为BONUS的模块。
其功能是为销售额最高的部门的雇员的薪水增加$2,000,但是如果某个符合条件的雇员的当前工资己经达到或超过了$150,000,则薪水只增加$1000。

2 对模块的输入情况如图5-1中的表格所示。
如果模块正确完成了其功能,返回错误代码
0。
如果雇员或部门表中不存在任何条目,模块将返回错误代码l。
如果在某个符合条件的部门中未发现任何雇员,模块将返回错误代码
2。
图5-1BONUS模块的输入表 模块的源程序如图5-2所示。
输入参数ESIZE和DSIZE分别代表雇员表和部门表内条目的数量。
该模块是用PL/l语言编写的,但下面的讨论整体上是与编程语言无关的;这些技术可以用在其他语言编写的程序中。
同时,由于本模块中的PL/1逻辑结构非常简单,实际上任何读者,甚至不熟悉PL/l的人也应该能够读懂程序。
2这三个数据与图5-2源程序中的数据不一致,图是$200,$100。
——译者注。
未经同意,严禁以任何形式拷贝 72 软件测试的艺术 BONUS:PROCEDURE(EMPTAB,DEPTTAB,ESIZE,DSIZE,ERRCODE); DECLARE1EMPTAB(*), 2NAMECHAR
(6), 2CODECHAR
(1), 2DEPTCHAR
(3), 2SALARYFIXEDDECIMAL(7,2); DECLARE1DEPTTAB(*), 2DEPTCHAR
(3), 2SALESFIXEDDECIMAL(8,2); DECLARE(ESIZE,DSIZE)FIXEDBINARY; DECLAREERRCODEFIXEDDECIMAL
(1); DECLAREMAXSALESFIXEDDECIMAL(8,2)INIT
(0);/*MAX.SALESIN DEPTTAB*/ DECLARE(
I,J,K)FIXEDBINARY;/*COUNTERS*/ DECLAREFOUNDBIT
(1);/*TRUEIFELIGIBLEDEPT.HASEMPLOYEES*/ DECLARESINCFIXEDDECIMAL(7,2)INIT(200.00);/*STANDARDINCREMENT*/ DECLARELINCFIXEDDECIMAL(7,2)INIT(100.00);/*LOWERINCREMENT*/ DECLARELSALARYFIXEDDECIMAL(7,2)INIT(15000.00);/*SALARY BOUNDARY*/ DECLAREMGRCHAR
(1)INIT('M'); 1ERRCODE=0; 2IF(ESIZE<=0)|(DSIZE<=0) 3THENERRCODE=1;/*EMPTABORDEPTTABAREEMPTY*/ 4ELSEDO;
5 DOI=1TODSIZE;/*FINDMAXSALESANDMAXDEPTS*/
6 IF(SALES(I)>=MAXSALES)THENMAXSALES=SALES(I);
7 END;
8 DOJ=1TODSIZE;
9 IF(SALES(J)=MAXSALES)/*ELIGIBLEDEPARTMENT*/ 10 THENDO; 11 FOUND='0'B; 12 DOK=1TOESIZE; 13 IF(EMPTAB.DEPT(K)=DEPTTAB.DEPT(J)) 14 THENDO; 15 FOUND='1'B; 16 IF(SALARY(K)>=LSALARY)|CODE(K)=MGR) 17 THENSALARY(K)=SALARY(K)+LINC; 18 ELSESALARY(K)=SALARY(K)+SINC; 19 END; 20 END; 21 IF(-FOUND)THENERRCODE=2; 22 END; 23 END; 24 END; 25END; 图5-2BONUS模块 无论采用哪种逻辑覆盖方法,第一步都是要列举出程序中所有的条件判断。
该 程序中需要列出的对象是所有的IF和DO语句。
通过对程序进行检查,我们可以看 出所有的DO语句都是此简单的迭代,每一个迭代的上限都等于或大于初始值(意 味着每个循环体总是会至少执行一次),而且退出循环的惟一方法是通过DO语句。
未经同意,严禁以任何形式拷贝 73 第5章模块(单元)测试 因此,该程序中的DO语句无须特别关注,因为任何导致DO语句执行的测试用例最终都会使其进入两个方向的分支路径(即进入循环体和退出循环体)。
因此,必 须要进行分析的语句有: 2IF(ESIZE<=O)|(DSIZE<=0)6IF(SALES(I)>=MAXSALES)9IF(SALES(J)=MAXSALES)13IF(EMPTAB.DEPT(K)=DEPTTAB.DEPT(J))16IF(SALARY(K)>=LSALARY)|(CODE(K)=MGR)21IF(-FOUND)THENERRCODE=
2 得到了数量较少的判断后,我们可能会选择多重条件覆盖,但应该检查所有的 逻辑覆盖准则(语句覆盖准则除外,其限制太多以至于不易于使用),看看它们的 效果如何。
为了满足判定覆盖准则,我们需要设计充足的测试用例,来触发上述6个判断中每一个的全部输出结果。
触发所有判定输出所需的输入状态列举在表5-1中。
由于有两个输出结果总会发生,故需要测试用例触发的状态只有10个。
注意,要建立表5-l,必须沿程序逻辑结构对判定输出的状态进行回溯,以判别相应的正确输入状态。
例如,判断16不会被任何符合条件的雇员触发。
雇员必须处在满足条件的部门之内。
表5-1与判定输出对应的状态 DecisionTruee
2 ESIZEorDSIZE≤
0.
6 Willalwaysuratleastonce.
9 Willalwaysuratleastonce 13 Thereisanemployeeinaneligible department. 16 Aneligibleemployeeiseithera managerorearnsLSALARYor more. 21 Alleligibledepartmentscontainno employees. FalseeESIZEandDSIZE>
0.OrderDEPTTABsothatadepartmentwithlowersalesursafteradepartmentwithhighersales.Alldepartmentsdonothavethesamesales.Thereisanemployeewhoisnotinaneligibledepartment.AneligibleemployeeisnotamanagerandearnslessthanLSALARY.Aneligibledepartmentcontainsatleastoneemployee. 表5-l中关注的10种情况可以被图5-3中所示的两个测试用例触发。
注意,每个测试用例都包含了对预期输出的定义,这是符合本书第2章讨论的测试原则的。
未经同意,严禁以任何形式拷贝 74 软件测试的艺术 测试用例
1 输入 ESIZE=0Allotherinputsareirrelevant
2 ESIZE=DSIZE=
3 EMPTAB JONESED4221,000.00 SMITHED3214,000.00 LORINED4210,000.00 DEPTTAB D4210,000.00D328,000.00D9510,000.00 预期的输出 ERRCODE=1ESIZE,DSIZE,EMPTAB,andDEPTTABareunchangedERRCODE=2ESIZE,DSIZE,andDEPTTABareunchangedEMPTABJONESED4221,100.00SMITHED3214,000.00LORINED4210,200.00 图5-3满足判定覆盖准则的测试用例 尽管这两个测试用例都满足判定覆盖准则,但很明显,该模块中仍可能存在着很多类型的错误不能通过这两个用例来发现。
举例来说,这两个用例没有对错误代码为
0、某名雇员是管理人员或部门表为空(DSIZE<=0)时的情况进行检查。
使用条件覆盖准则可以获得更为满意的测试效果。
因此我们需要设计出足够的 测试用例,来触发判断中每个条件的所有输出结果。
触发所有输出结果的条件以及 所需的输入情况列在表5-2中。
由于两个输出结果总会出现,需要测试用例强制触 发的状态有14个。
这些状态同样可以仅被两个测试用例触发,如图5-4所示。
测试 输入 用例 预期的输出 1ESIZE=DSIZE=
0 ERRCODE=1ESIZE,DSIZE,EMPTAB,and Allotherinputsareirrelevant DEPTTABareunchanged
2 ESIZE=DSIZE=
3 ERRCODE=
2 EMPTAB DEPTTAB ESIZE,DSIZE,andDEPTTABare JONESED4221,000.00D4210,000.00unchanged SMITHED3214,000.00D328,000.00EMPTAB LORINED4210,000.00D9510,000.00JONESED4221,100.00 SMITHED3214,000.00 LORINED4210,200.00 图5-4满足条件覆盖准则的测试用例 图5-4所示的测试用例是用来说明某个问题的。
由于他们的确可以触发表5-2中列举的所有输出结果,因此它们满足条件覆盖准则,但是从满足判定覆盖准则的 未经同意,严禁以任何形式拷贝 75 第5章模块(单元)测试 效果来看,它们要比图5-3中的测试用例集差一些。
原因是它们没有执行到每条语句。
举例来说,语句18就从没有被执行过。
而且,它们也不比图5-3中的测试用例更全面,它们没有触发输出ERRORCODE=
0。
如果语句2错误地编写成(ESIZE=0)并且(DSIZE=0),这个错误就检查不出来。
当然,其他的测试用例集也可能会解决这个问题,但是图5-4中的两个测试用例确实可以满足条件覆盖准则,这个事实是存在的。
使用判断/条件覆盖准则可以克服图5-4中的测试用例存在的明显缺陷。
这里,我们会提供足够多的测试用例,使得所有条件和判断的全部输出结果都至少被触发一次。
让Jones当管理人员,Lorin当非管理人员,就可以实现这一点。
这样做的结果是,将产生判断16的两个输出结果,从而使语句18得到执行。
然而,这样做存在一个问题,从根本上讲它并不比图5-3中的测试用例更完善。
如果我们使用的编译器一旦判断出“或”表达式中某个操作对象结果为真,就停止检查该表达式,那么就会导致语句16中CODE(K)=MGR表达式永远得不到为真的结果。
因此,如果该表达式编码不正确,这些测试用例无法发现此错误。
我们要讨论的最后一个准则式多重条件覆盖准则。
这个准则要求设计出足够多的测试用例,以便将每个判断中的所有可能的条件组合至少触发一次。
这项工作可以从表5-2开始。
判断6、9、13和21每个都有两种组合;而判断2和16每个都有4种组合。
设计测试用例的方法是挑选出一个测试用例覆盖到尽可能多的组合情况,然后再挑选出一个测试用例覆盖到尽可能多的剩余组合情况,以此类推。
满足多重条件覆盖准则的测试用例集如图5-5所示。
这个集合要比前面的测试用例集全面得多,也就意味着,我们应该开始就选择多重条件覆盖准则。
表5-2与条件输出对应的状态 DecisionCondition
2 ESIZE≤
0 2 DSIZE≤
0 6 SALES(I)≥MAXSALES TrueeESIZE≤0DSIZE≤0Willalwaysuratleastonce.
9 SALES(J)=MAXSALES 未经同意,严禁以任何形式拷贝 Willalwaysuratleast Falsee ESIZE>
0 DSIZE>
0 OrderDEPTTABsothatadepartmentWithlowersalesursafteradepartmentwithhighersales. Alldepartmentsdonothavethesame 76 软件测试的艺术 once. sales. 13 EMPTAB.DEPT(K)= Thereisanemployee DEPTTAB.DEPT(J) Thereisanemployeewhoinaneligibleisnotinandepartment.Eligibledepartment. 16 SALARY(K) ≥LSALARY AneligibleemployeeearnsLSALARYormore. AneligibleemployeeearnslessthanLSALARY. 16 CODE(K)=MGR Aneligibleemployeeisamanager. Aneligibleemployeeisnotamanager. 21 -FOUND Aneligibledepartmentcontainsnoemployees. Aneligibledepartmentcontainsatleastoneemployee. 测试用例
1 输入 ESIZE=0DSIZE=0Allotherinputsareirrelevant
2 ESIZE=0DSIZE>
0 Allotherinputsareirrelevant3ESIZE>0DSIZE=
0 Allotherinputsareirrelevant
4 ESIZE=5DSIZE=
4 EMPTAB DEPTTAB JONESMD4221,000.00D4210,000.00 WARNSMD9512,000.00D328,000.00 LORINED4210,000.00D9510,000.00 TOYED9516,000.00SMITHED3214,000.00 D4410,000.00 图5-5满足多重条件覆盖准则的测试用例 预期的输出 ERRCODE=1ESIZE,DSIZE,EMPTAB,andDEPTTABareunchangedSameasabove Sameasabove ERRCODE=2ESIZE,DSIZE,andareunchangedEMPTABJONESMD42WARNSMD95LORINED42TOYED95SMITHED32 DEPTTAB 21,100.0012,100.0010,200.0016,100.0014,000.00 BONUS模块可能存在着大量的错误,即使是满足多重条件覆盖准则的测试用例也检查不出来,认识到这一点很重要。
举例来说,没有测试用例会触发ERRORCODE返回值为0的情况,因此,如果语句1漏掉了,该错误就发现不到。
如果LSALARY被错误地初始化为$150,000.01,这个错误也将无法发现。
如果语句16中写的是SALARY(K)>LSALARY而不是SALARY(K)>=LSALARY,这个错误 未经同意,严禁以任何形式拷贝 77 第5章模块(单元)测试 也将无法发现。
同样,各种off-by-one错误(例如没有正确地处理DEPTTAB或EMPTAB表中最末的条目)能否检查出来,在很大程度上全凭运气。
现在有两个观点清晰了:多重条件覆盖准则优先于其它准则;任何逻辑覆盖准则尚不足以胜任作为生成模块测试用例的惟一手段。
因此,下一个步骤就是用一组黑盒测试用例来补充图5-5中的测试用例。
要做到这一点,我们将BONUS模块的接口规格说明列举在下面: PL/1模块BONUS接收5个参数,分布是EMPTAB,DEPTTAB、ESIZE、DSIZE 和ERRORCODE。
这些参数的属性如下: DECLARE1EMPTAB(*),/*INPUTANDOUTPUT*/2NAMECHARACTER
(6),2CODECHARACTER
(1),2DEPTCHARACTER
(3),2SALARYFIXEDDECIMAL(7,2); DECLARE1DEPTTAB(*),/*INPUT*/2DEPTCHARACTER
(3),2SALESFIXEDDECIMAL(8,2); DECLARE(ESIZE,DSIZE)FIXEDBINARY;/*INPUT*/DECLAREERRCODEFIXEDDECIMAL
(1);/*OUTPUT*/ 模块假设传递的参数具有以上属性。
ESIZE,DSIZE分别表示EMPTAB、DEPTTAB 表中的条目数量,而EPTAB、DEPTTAB表中的条目顺序未作任何假设。
该模块的功 能是为销售额(DEPTTAB、SALES)最高的一个或多个部门的雇员增加薪水(EMPTAB, SALARY)。
如果某个符合条件的雇员当前工资已经达到或超过了$150,000,或者该 雇员为管理人员(EMPTAB.CODE=‘M’),则薪水只增加$1,000,否则增加$2,000。
模块假设增加的薪水放入EMPTAB.SALARY中。
如果ESIZE、DSIZE不大于
0,则将 ERRCODE设置为
1,并且不进行任何操作。
在所有其他情况下,完整地执行模块的 功能。
然而,如果某个具有最大销售额的部门没有雇员,程序继续执行,但将ERRCODE 设置为2;否则设置为
0。
上述规格说明并不适合于因果图分析方法(没有一组应检查其组合情况的能力 分辨出来的输入条件),因此采用边界值分析方法。
确定的输入边界如下:
1.EMPTAB中条目数量为
1。

2.EMPTAB中条目数量为最大值(65535)。

3.EMPTAB中条目数量为
0。

4.DMPTAB中条目数量为
1。
未经同意,严禁以任何形式拷贝 78 软件测试的艺术
5.DMPTAB中条目数量为最大值(65535)。

6.DMPTAB中条目数量为
0。

7.某个销售额最高的部门仅有1名雇员。

8.某个销售额最高的部门有65535名雇员。

9.某个销售额最高的部门没有雇员。
10.DEPTTAB中所有部门的销售额相同。
11.销售额最高的部门为DEPTTAB的第一条目。
12.销售额最高的部门为DEPTTAB的最末条目。
13.某个符合条件的雇员为EMPTTAB的第一条目。
14.某个符合条件的雇员为EMPTTAB的最末条目。
15.某个符合条件的雇员为管理人员。
16.某个符合条件的雇员不是管理人员。
17.某个不为管理人员、且符合条件的雇员的薪水为$149999.99。
18.某个不为管理人员、且符合条件的雇员的薪水为$150000。
19.某个不为管理人员、且符合条件的雇员的薪水为$150000.01。
输出边界如下:20.ERRCODE=
0。
21.ERRCODE=
1。
22.ERRCODE=
2。
23.某个符合条件雇员增加后的薪水为$299999.99。
使用错误猜测技术进一步确定的测试条件如下:24.在DEPTTAB中,某个没有雇员的销售额最大的部门其后跟着另一个有雇 员的销售额最大的部门。
这用来判断当遇到ERRCODE=2的情况时,模块是否错误地终止对输入的处理。
评价一下这24种条件,其中条件2、5和8看起来像是不切实际的测试用例。
由于它们所代表的条件不可能发生(在测试中作这种假设通常是很危险的,但在此 处似乎比较安全),因此可以将它们排除掉。
下一步是将剩下的21种条件与当前的 测试用例集(图5-5)进行比较,判断哪些边界条件尚未被覆盖到。
通过比较,我 未经同意,严禁以任何形式拷贝 79 第5章模块(单元)测试 们发现需要为条件l、4、7、10、14、17、18、19、20、23和24设计图5-5中没有的测试用例。
再下一步是设计额外的测试用例以覆盖上述11种边界条件。
一种方法是将这些条件合并到现有的测试用例中去(即对图5-5中的第4个用例进行修改),但我们不推荐这样做,因为这样做会打乱现有测试用例完整的多重条件覆盖。
因此,最保险的方法是增加图5-5之外的测试用例。
在这样做的过程中,我们的目标是使设计覆盖边界条件所需的最小数量的测试用例。
图5-6中的三个测试用例实现了这一点。
测试用例5覆盖了条件7、10、14、17、18、19和20,测试用例6覆盖了条件l、4和23,而测试用例7则覆盖了条件24。
在这里我们有理由相信,逻辑覆盖准则或白盒测试用例、图5-6所示的测试用 例已实现对模块BONUS适度的模块测试。
测试 输入 用例
5 ESIZE=3DSIZE=
2 EMPTAB DEPTTAB ALLYED3614,999.99D3355,400.01 BESTED3315,000.00D3655,400.01 CELTOED3315,000.01
4 ESIZE=1DSIZE=
1 EMPTAB DEPTTAB CHIEFMD9998,899.99D9999,000.00
4 ESIZE=2DSIZE=
2 EMPTAB DEPTTAB DOLEED6710,000.00D6620,000.00 WARNSED2233,333.33D6720,000.00 预期的输出 ERRCODE=0ESIZE,DSIZE,andareunchangedEMPTABALLYED36BESTED33CELTOED33ERRCODE=0ESIZE,DSIZE,andareunchangedEMPTABCHIEFMD99ERRCODE=2ESIZE,DSIZE,andareunchangedEMPTABDOLEED67WARNSED22 DEPTTAB 15,199.9915,100.0015,100.01DEPTTAB 99,899.99DEPTTAB 10,000.0033,333.33 图5-6BONUS模块补充的边界值分析测试用例 5.2增量测试 在执行模块测试过程中,我们主要有两点考虑:第
一,如何设计一个有效的测试用例集,这在上一节已经讨论过。

二,将模块组装成工作程序的方式。
第二点考 未经同意,严禁以任何形式拷贝 80 软件测试的艺术 虑很重要,因为它涉及模块测试用例编写的形式、可能用到的测试工具类型、模块编码和测试的顺序、生成测试用例的成本以及调试(定位并修复检查出的错误)的成本等。
简而言之,它具有实际重要性。
在这一节中,我们将讨论两类方法,增量测试和非增量测试。
在下一节中,我们将探讨两种增量方法:自顶向下的和自底向上的开发或测试过程。
这里需要考虑的问题是:软件测试是否应先独立地测试每个模块,然后再将这些模块组装成完整的程序?还是先将下一步要测试的模块组装到测试完成的模块集合中,然后再进行测试?第一种方法称为非增量测试或“崩溃(big-bang)”测试,而第二种方法称为增量测试或集成。
图5-7所示的程序可作为一个例子。
矩形框代表程序的6个模块(子程序或过程),连接模块间的线条代表程序的控制层次,也就是说,模块A调用模块
B、C和
D,模块B调用模块E等等。
作为传统方法的作增量测试是按如下方式进行的:首先,对6个模块中的每一个模块进行单独的模块测试,将每个模块视为一个独立实体。
根据环境(例如,是人机交互式的,还是使用批处理计算工具)和参与人数,这些模块可以同时或按次序进行测试。
最后,将这些模块组装或集成(例如“连接编辑”)为完整的程序。
图5-7包含6个模块的程序范例 测试单独的模块需要一个特殊的驱动模块(drivemodule)和一个或多个桩模块(stubmodule)。
举例来说,测试模块
B,首先要设计测试用例,然后将测试用例作 未经同意,严禁以任何形式拷贝 81 第5章模块(单元)测试 为输入参数由驱动模块传递给模块
B。
驱动模块是人们编写的一个小模块,用来将测试用例驱动或传输到被测模块中(也可以用测试工具替代)。
驱动模块还必须向测试人员显示模块B的结果。
此外,由于模块B调用了模块
E,所以还必须使用一个额外的组件,该组件在模块B调用模块E时接受模块B的控制指令。
这就由桩模块来完成它是一个被命名为“E”的特殊模块,用来模拟模块E的功能。
当所有6个模块的模块测试都完成之后,就将这些模块组装成完整的程序。
另一种可选择的方法是增量测试。
不同于独立地测试每个模块,增量测试首先将下一个要测试的模块组装到前面已经测试过的模块集合中去。
现在要给出对图5-7所示的程序进行增量测试的步骤还为时太早,因为还有大 量可能的增量方法。
一个关键问题是我们究竟是从程序的顶部开始、还是从底部开 始进行测试。
由于这个问题将在下一节中讨论,我们暂且假设从底部开始测试。
第 二步先测试模块
E、C和
F,可以并行测试(由三个人进行),也可串行进行。
请注 意,我们必须要为每个模块准备一个驱动模块,但不是桩模块。
下一步是测试模块 B和
D,但不是单独地测试它们,而是分别将其与模块E和F组装在一起。
换言之, 要测试模块
B,应编写驱动模块和集成测试用例,将模块B和E组合起来测试.将 下一个要测试的模块组装到前面已经测试过的模块集合或子集中去,这个增长的过 程会一直进行到测试完最后一个模块(本例中是模块A)为止,请注意,这个过程 也可以自顶向下进行。
下面是几个显而易见的结论:
1.非增量测试所需的工作量要多一些。
对于图5-7所示的程序,需要准备
5 个驱动模块和5个桩模块(假设顶部的模块不需要驱动模块)。
自底向上的 增量测试需要5个驱动模块,但不需要桩模块。
自顶向下的增量测试需要 5个桩模块,但不需要驱动模块。
增量测试所需的工作量要少一些,因为 使用了前面测试过的模块来取代非增量测试中所需要的驱动模块(如果从 顶部开始测试)或桩模块(如果从底部开始测试)。

2.

如果使用了增量测试,可以较早地发现模块中与不匹配接口、不正确假设 相关的编程错误。
这是由于尽早地对模块组合进行了集成测试。
然而,如 果采用非增量测试,只有到了测试过程的最后阶段,模块之间才能“互相 看到”。

3.

因此如果使用了增量测试,调试会进行得容易一些,我们假定存在着与模 未经同意,严禁以任何形式拷贝 82 软件测试的艺术 块间接口或假设相关的编程错误(根据经验而来的合理假设),那么,如果使用非增量测试,直到整个程序组装之后,这些错误才会浮现出来。
到了这个时候,我们就难以定位错误。
因为它可能存在于程序内部的任何位置,相反,如果使用增量测试,这种类型的错误就很容易发现,因为该错误很可能与最近添加的模块有关。

4.增量测试会将测试进行得更彻底。
如果当前正在测试模块
B,要么是模块
E,要么是模块A(取决于测试是从底部还是从顶部开始的)被当作结果而执行。
虽然模块E或模块A先前已经进行了完全的测试,但将其作为B的模块测试结果而执行,则会诱发出一个新的情况,可能会暴露出先前测试过的模块E或模块A中存在的一个新缺陷。
另一方面,如果使用的是非增量测试,对模块B的测试仅影响到其本身。
换言之,增量测试使用先前测试过的模块,取代了非增量测试中使用的桩模块或驱动模块。
因此,到最后一个模块测试完成时,实际的模块经受到了更多的检验。

5.非增量测试所占用的机器时间显得少一些。
如果使用自底向上的方法测试图5-7中的模块
A,在执行A的过程中,模块
B、C、
D、E和F也会执行到。
而在对模块A的非增量测试中,仅会执行模块
B、C和E的桩模块。
自顶向下的增量测试的情况也是如此。
如果测试的是模块
F,那么在执行模块F时还会执行模块
A、B、
C、D和E;而在对模块F的非增量测试中,仅有模块F的驱动模块与其一起执行。
因此,完成一次增量测试所需执行的机器指令,显然多于采用非增量测试方法所需的指令。
但此消彼长的是,非增量测试要比增量测试需要更多的驱动模块和桩模块,开发这些驱动模块和桩模块是要占用机器时间的。

6.模块测试阶段开始时,如果使用的是非增量测试,就会有更多的机会进行并行操作(也就是说,所有的模块可以同时测试)。
对于大型的软件项目〔模块和人员都很多),这可能十分重要,因为在模块测试开始之时,项目的人员数量常常处于最高峰。
总的来说,第1条~第4条结论是增量测试的优点,而第5、6条结论是其不利之处。
考虑到计算机行业当前的趋势(硬件成本已经降低而且势必会持续下去,硬件的功能不断增加,而人力劳动成本和软件错误的代价在不断增长),再考虑到 未经同意,严禁以任何形式拷贝 83 第5章模块(单元)测试 错误发现得越早,改正它的成本也越低,我们会看到第1条至第4条结论的重要性日益突出,而第5条结论越来越显得不那么重要。
如果有一个缺点的话,第6条结论似乎确是一个薄弱的缺点。
从而我们可以得出结论,增量测试要更好一些。
5.3自顶向下测试与自底向上测试 在上一节结论的基础上,即增量测试要优于非增量测试,本节将讨论两种增量测试策略:自顶向下的测试和自底向上的测试。
然而在讨论它们之前,先要澄清几个误解。
首先,“自顶向下的测试”、“自顶向下的开发”和“自顶向下的设计”常用作近义词。
“自顶向下的测试”和“自顶向下的开发”确实是同义词(表示安排模块的编码和测试顺序的策略),但“自顶向下的设计”则完全不同并且是独立的概念,按自顶向下模式设计的程序既可使用自顶向下的方式,也可使用自底向上的方式进行增量测试。
其次,自底向上的测试(或自底向上的开发)常被错误地当作非增量测试。
原因在于自底向上的测试的开展方式与非增量测试是相同的(即对底层或终端模块进行测试),但是就如我们从上一节看到的那样,自底向上的测试是一种增量测试。
最后,由于两种策略都属于增量测试,因此增量测试的优点在这里就不再赘述,仅讨论自顶向下测试与自底向上测试的差异。
5.3.1自顶向下的测试 自顶向下的测试是从程序的顶部或初始模块开始。
测试开始之后,挑选哪一个后续模块进行增量测试没有惟一正确的方法:惟一的原则是:要成为合乎条件的下一个模块,至少一个该模块的从属模块(调用它的模块)事先经过了测试。
我们用图5-8来说明这种测试策略。
A至L代表程序的12个模块。
假定模块J包含程序的I/O读操作,而模块I包含I/O写操作。
第一步是测试模块
A,测试要求必须编写出代表
B、C和D的桩模块。
遗憾的是,我们经常会错误理解桩模块的生成。
作为佐证,我们可能经常会听到这样的说 未经同意,严禁以任何形式拷贝 84 软件测试的艺术 法,“一个桩模块仅需要写一条‘我们进行到了这一步’的信息”、“在很多情况下,模拟的桩模块仅仅只是存在而不起任何作用”。
在大多数情况下,这些说法都是错误的。
由于模块A调用模块
B,模块A就需要模块B执行一些操作,这些操作很可能就是返回给模块A的结果(输出参数)。
如果桩模块仅仅只是返回了控制,或显示一条出错信息却没有返回一个有意义的结果,模块A就会发生失效,这并不是由于模块A存在错误,而是因为桩模块未能模拟出相应的模块。
此外,桩模块仅仅返回一个“已经连通(wired-in)”的结论是不够的。
举例来说,让我们考虑编写一个桩模块,代表一个平方根程序、一个数据库表搜索程序,一个“获取相关主文件记录”程序或诸如此类的程序等。
如果这个桩模块仅仅返回一条固定的“已经连通”输出,却没有返回调用模块此次调用所希望的特定值,那么调用模块将会发生失效或是产生一个混乱的结果。
因此,编写桩模块是很关键的。
图5-8包含12个模块的程序范例 另一个需要考虑的地方是采取了什么样的形式将测试用例提交给程序,这是一个非常重要的问题,大多数对自顶向下测试的研究都没有提到这一点。
在我们给出的例子中,存在这样的问题:如何向模块A提交测试用例?由于在典型的程序中,顶部模块既不接收输入参数,也不执行输入/输出操作,因此问题的答案不是显而易见的。
答案是:测试数据是通过其一个或多个桩模块提交给模块(此处为模块A)的。
为了说明这一点,假设模块
B、C和D的功能如下: 未经同意,严禁以任何形式拷贝 85 第5章模块(单元)测试 B―获取事务文件的概要。
C―判断每周的状态是否满足限额。
D―生成每周总结报告。
那么自桩模块B返回的一个事务概要就是模块A的一个测试用例。
桩模块D可能包含将其输入数据写到打印机的语句,这样就可以检查每一个测试的结果。
在本程序中还存在另一个间题。
由于假定模块A仅调用模块B一次,问题是如何将多个测试用例提交给模块
A。
一个解决方法是编写出桩模块B的多个版本,每一个版本都将一个各不相同的有效测试数据集返回给模块
A。
为了执行这些测试用例,程序需要执行多次,每次都使用桩模块B的不同版本。
另一种可选择的方法是将测试数据放置在外部文件中,由桩模块B读取并返回给模块
A。
根据前面的讨论,对于任何一种情况,开发桩模块通常要比实际理解的更为困难。
而且,由于程序的特点所致,通过被测模块之下的多个桩模块来传送测试数据常常是必需的(即被测模块通过调用多个桩模块来获得要处理的测试数据)。
模块A测试完成之后,就用一个实际的模块代替其中的一个桩模块,而该模块需要的桩模块也被添加进来。
举例来说,图5-9就显示了该程序的下一个版本。
图5-9自顶向下测试的第二个步骤 测试完顶部模块之后,接下来可能的测试序列有很多。
举例来说,如果我们要执行所有的测试序列,大量可能的模块序列中的四个序列如下:
1.ABCDEFGHIJKL2.ABEFJCGKDHLI 未经同意,严禁以任何形式拷贝 86 软件测试的艺术
3.ADHI4.ABFJ KLCGBFJEDIECGKHL 如果可以进行并行测试,可能还有其他的选择。
举例来说,模块A测试结束之后,一位程序员可能会选取模块
A,测试模块A-B的组合,另一位程序员可能会测试模块A-C的组合,而第三位程序员可能会测试模块A-D的组合。
总的来说,不存在最佳的模块序列,但却有下面可供考虑的两项指南:
1.如果程序中存在关键部分(例如模块G),那么在设计模块序列时就应将这些关键模块尽可能早地添加进去。
所谓“关键部分”可能是某个复杂的模块、某个采用新算法的模块或某个被怀疑容易发生错误的模块。

2.在设计模块序列时,应将I/O模块尽可能早地添加进来。
第一项指南的动机非常清楚,但第二项指南的动机则需要进一步的讨论。
回想一下,桩模块的问题就是一部分桩模块须包含测试用例,而另一部分桩模块则须将其输入写到打印机中或显示出来。
然而,接收程序输入的模块一旦被添加进来,测试用例的描述就相当简单了,其采用的形式就与最终程序接收的输入一样(例如,通过事务文件或终端)。
相似地,一旦执行程序输出功能的模块被添加进来,桩模块中就可能无需再放置输出测试用例结果的代码。
因此,如果模块J和模块I是I/O模块,而模块G执行某些关键操作,那么增长序列可能是: ABFJDICGEKHL而第6个增量3之后,程序可能是如图5-10所示的形式。
一旦到达了如图5-10所示的中间阶段,测试用例的描述以及测试结果的检查就简单化了。
由于有一个程序实际运行的框架版本,也就是执行实际的输入和输出操作,就带来了另一个好处。
然而,桩模块依然模拟着部分“内幕”。
这个早期的程序框架版本有以下优点: •可以使我们发现人为因素的错误和问题。
•可以将程序演示给最终用户看。
•证明程序的整体设计是合理的。
3即加入
I。
——译者注 未经同意,严禁以任何形式拷贝 87 第5章模块(单元)测试 •起到精神上的鼓舞作用。
然而另一方而,自顶向下策略还有一些严重缺陷。
假定我们当前的测试状态如图5-10所示,下一步是用模块H取代桩模块
H。
这时(或更早一些)我们所要做的是使用本章前面所述的方法,为H设计一个测试用例集。
但是请注意,这些测试用例采用的是向模块J的实际程序输入的形式。
这带来了一些问题。
首先,由于在模块J和模块H之间存在中间摸块(即模块
F、B、A和D),我们会发现无法将测试过模块H中所有预先确定的情况的测试用例提交到模块J中去。
举例来说,如果H是如图5-2所示的BONUS模块,由于中间模块D的存在,就无法生成图5-5和图5-6中的7个测试用例中的部分用例。
其次,由于H和程序中测试数据引入点之间存在着“距离”,即使存在着测试全部状态的可能性,要决定往模块J中输入什么样的数据来测试到H中的所有状态,通常也是一项困难的脑力劳动。
最后,由于一个测试显示出来的输出可能来自于一个与被测模块相距甚远的模块,要将显示出来的输出与此模块的实际执行情况联系起来非常困难,甚至是不可能的。
想象一下将模块E添加到图5-10中,每个测试用例的结果都取决于检查模块I的输出,但是由于存在着中间模块,要推演出模块E的实际输出(即返回给模块B的数据)可能是很困难的。

A B StubC
D StubE
F StubH
I J 图5-10自顶向下测试的中间状态 未经同意,严禁以任何形式拷贝 88 软件测试的艺术 自顶向下的测试策略取决于其使用的方法,可能还存在两个更深层次的问题。
人们会偶尔感觉到它可能与程序的设计阶段重叠。
举例来说,如果我们正在设计如图5-8所示的程序,可能会觉得在最先的两个层次设计完成之后,在下面层次的设计进行的同时就可以对模块A至模块D进行编码和测试了。
正如我们在其他地方所强调的那样,这往往不是明智之举。
程序设计是一个迭代的过程,这意味着当我们在设计程序结构的较低层次时,可能会对较高层次进行合理的变更或改进。
如果程序的较高层次已经完成了编码和测试,那么这些理想的改进就会被摒弃,最终成为一个不明智的决策。
实践中时常会发生的一个终极问题是,在进行到下一个模块前未能穷举测试此模块。
这来自于两个原因:一是由于将测试数据嵌入桩模块中存在困难,二是由于程序的较高层次通常会为较低层次提供资源。
在图5-8中,我们看到,对模块A的测试需要用到针对模块B的多个版本的桩模块。
在实践中,我们会倾向于说“由于这需要投人很多工作,我现在就不执行模块A的所有测试用例,一直等到将模块J添加到程序中,此时引入测试用例就容易多了,我会记得在那时完成对模块A的测试”。
当然,这里的问题是到了那个较晚的时间点,我们可能会忘记模块A中剩下的测试。
另外,因为较高的层次常常会提供资源给较低层次(例如打开文件)使用,有时除非到了使用资源的低层次模块测试完成之后,我们很难判断这些资源提供得是否正确(例如,文件是否以正确的属性打开)。
5.3.2自底向上的测试 下面讨论自底向上的增量测试策略。
在大多数情况下,自底向上的策略与自顶向下的策略是相对立的。
自顶向下测试的优点成为自底向上测试的缺点,而自顶向下测试的缺点又成为自底向上测试的优点。
正因为这一点,我们对自底向上测试的介绍就简短一些。
自底向上的策略开始于程序中的终端模块(此类模块不再调用其他任何模块)。
测试完这些模块之后,同样没有最佳的方法来挑选要进行增量测试的下一个模块。
惟一正确的原则是,要成为合乎条件的下一个模块,该模块所有的从属模块(它调 用的模块)都已经事先经过了测试。
回到图5-
8,第一步是测试模块
E、J、
G、K、 L和I中的部分或全部模块,既可以串行进行,也可以并行进行。
要做到这一点, 未经同意,严禁以任何形式拷贝 89 第5章模块(单元)测试 每一模块都需要一个特殊的驱动模块:即包含着有效的测试输入、调用被测模块且将输出显示出来(或将实际输出与预期输出作比较)的模块。
有别于使用桩模块的情况,由于驱动模块可以交迭地调用被测模块,因此不需要为驱动模块提供多个版本。
在大多数情况下,开发驱动模块要比开发桩模块更容易些。
如同前面的例子一样,影响测试序列的因素是模块的关键程度。
如果我们觉得模块D和模块F最为关键,那么应该自底向上增量测试的某个中间状态可能如图5-11所示。
接下来的步骤可能是测试模块
E,然后再测试模块
B,将模块B与先前测试过的模块
E、F和J组装起来进行测试。
Drive Drive
F D
J H
I J
L 图5-11自底向上测试的中间状态 自底向上策略的一个不足是,它没有早期程序框架的概念。
事实上,直到最后一个模块(模块A)被添加进来,才形成了可工作的程序,也就是完整的程序。
尽管I/O功能可以在整个程序集成之前进行测试(I/O模块在图5-11中用到),早期程序框架的优点在这里体现不出来。
自顶向下方法中无法建立所有测试环境的问题,在这里都不复存在。
如果将驱 动模块看作是一个测试探针的话,那么该探针是直接放入被测模块中去的,不会受 到中间模块的困扰。
检查一下与自顶向下方法相关的其他问题,我们再也不会做出 让设计和测试重叠的不明智决定,因为自底向上的测试要直到程序底层设计完成之 后方才开始。
同样,在没有测试完一个模块之前就开始另一个模块测试的问题也不 未经同意,严禁以任何形式拷贝 90 软件测试的艺术 会存在,这是因为使用自底向上的测试不再有如何将测试数据绑定到桩模块中去的烦恼。
5.3.3比较 如果自顶向下的方法和自底向上的方法,就象增量测试和非增量测试一样区别分明,那么比较起来很容易,但遗憾的是,情况并非如此。
表5-3概括了它们之间相对的优点和不足(前面讨论过的两者皆有的优点除外,也就是增量测试的优点)。
每种方法的第一个优点似乎是决定性的因素,但是也没有证据表明主要的缺陷会更容易发生在典型程序的顶部或底层。
最保险的判断方法是,根据特定的被测程序,对表5-3中所示的各因素进行权衡。
由于这里缺乏一个规程,自顶向下测试第四个缺点的严重后果,以及有可用的测试工具减少了对驱动模块而不是桩模块的需求,这样似乎给自底向上的策略带来了优势。
除此之外,自顶向下的方法和自底向上的方法很显然都不是惟一可能的增量测试策路。
表5-3自顶向下测试与自底向上测试的比较 自顶向下测试 优点 缺点
1.如果主要的缺陷发生程序的顶层将非常
1.必须开发桩模块 有利
2.桩模块要比最初表现的更复杂
2.一旦引人I/O功能,提交测试用例会更容
3.在引入I/O功能之前,向桩模块中引入测 易 试用例比较用难
3.早期的程序框架可以进行演示,并可激发
4.创建测试环境可能很难,甚至无法实现 积极性
5.观察测试输出很困难
6.使人误解设计和测试可以交迭进行
7.会导致特定模块测试的完成延后 自底向上优点 优点 缺点
1.如果主要的缺陷发生在程序的底层将非
1.必须开发驱动模块 常有利
2.直到最后一个模块添加进去,程序才形成
2.测试环境比较容易建立 一个整体
3.观察测试输出比较容易 5.4执行测试 按下来介绍模块测试的其他部分如何实际进行测试。
这里我们给出了一系列操 作的提示和指南。
未经同意,严禁以任何形式拷贝 91 第5章模块(单元)测试 当测试用例造成模块输出的实际结果与预期结果不匹配的情况时,存在两个可能的解释:要么该模块存在错误,要么预期的结果不正确(测试用例不正确)。
为了将这种混乱降低到最小程度,应在测试执行之前对测试用例集进行审核或检查(也就是说,应对测试用例进行测试)。
使用自动化测试工具可以使测试过程中的枯燥劳动减至最小。
举例来说,现在已有测试工具可以降低我们对驱动模块的需求。
流程分析工具可以列举出程序中的路径、找出从未被执行的语句(“不可达”代码),以及找出变量在赋值前被使用的实例。
在准备模块测试时,重温一下本书第2章中讨论的心理学和经济学原则会有所裨益。
如同本章前面所做的那样,记住对预期输出进行定义是测试用例必不可少的部分。
在执行测试时,应该查找程序的副作用(即模块执行了某些不该执行操作的情况)。
一般情况下,这些情况都是很难发现的,但如果在测试用例执行完之后,检查那些不应有变动的模块输入,可能会发现一些错误实例。
举例来说,图5-7中的测试用例7声明ESIZE、DSIZE和DEPTTAB作为预期结果的一部分,不应发生变更。
在执行此测试用例时,不仅要检查输出结果是否正确,还要检查ESIZE、DSIZE和DEPTTAB,判断它们是否被错误地修改了。
因个人试图测试自己编写的程序所带来的心理学问题,也适用于模块测试。
程序员不应测试自己编写的模块,而应交换模块进行测试。
编写调用模块的程序员始终是测试被调用模块的最佳候选人。
注意,这仅仅适用于测试,对模块的调试一般应当由编程人员本人进行。
应避免随意丢弃测试用例,应将它们按某种格式记录下来,以便将来可以重新使用它们。
回想一下图2-2中那个有悖于直观的现象。
如果发现某一部分模块存在大量错误,那么很有可能这些模块甚至包含着更多的错误,只是尚未检查出来而已。
这样的模块应该进行更进一步的测试,可能还需要进行额外的代码走查或检查。
最后,记住模块测试的目的不是证明模块能够正确地运行,而是证明模块中存在着错误。
未经同意,严禁以任何形式拷贝 92 软件测试的艺术 第6章更高级别的测试 完成了对程序的模块测试之后,整个测试过程才刚刚开始,对于大型或复杂的软件来说尤为如此。
考虑下面这个重要概念: 当程序无法实现其最终用户要求的合理功能时,就发生了一个软件错误。
根据这个定义,即使完成了一次非常完美的模块测试,仍然不能保证已经找出了程序中的所有错误。
因此,要结束整个测试任务,还必须进行其他形式的更深入的测试。
我们将这些新形式的测试称为“更高级别的”测试。
软件开发过程在很大程度上是沟通有关最终程序的信息、并将信息从一种形式转换到另一种形式。
由于这个原因,绝大部分软件错误都可以归因为信息沟通和转换时发生的故障、差错和干扰。
未经同意,严禁以任何形式拷贝 93 第6章更高级别的测试 图6-1软件开发过程 图6-1描述了软件开发的这个观点,它表示了一个软件产品开发周期的模型。
过程的流程可归结为以下7个步骤:
1.将软件最终用户的要求转换为一系列书面的需求。
这些需求就是该软件产品要 实现的目标。

2.通过评估可行性与成本、消除相抵触的用户需求、建立优先级和平衡关系,将 用户需求转换为具体的目标。

3.将上述目标转换为一个准确的产品规格说明,将产品视为一个黑盒,仅考虑其 接口以及与最终用户的交互。
该规格说明被称为“外部规格说明”。

4.如果该产品是一个系统,如操作系统、飞行控制系统、数据库管理系统或雇员 人事系统等,而不仅是一个程序(编译器、工资程序、字处理程序等),那么下 未经同意,严禁以任何形式拷贝 94 软件测试的艺术 一步骤就是系统设计。
该步骤将系统分割为单独的程序、部件或子系统,并定义它们的接口。

5.通过定义每个模块的功能、模块的层次结构以及模块间的接口,来设计程序或程序集合的结构。

6.设计一份准确的规格说明,定义每个模块的接口与功能。

7.经过一个或更多的子步骤,将模块接口规格说明转换为每个模块的源代码算法。
以下是从其他角度来审视上述文档的形式:•需求规格说明定义了为什么要开发程序。
•目标定义了程序要做什么,以及应做得怎样。
•外部规格说明定义了程序对用户的准确表现。
•与后续阶段相关的文档越来越详细地规定了程序是如何建立起来的。
假定软件开发周期的七个阶段包括了信息的沟通、理解和转换,以及大多数的软件错误都来源于信息处理中的故障,那么现在有三个补充的方法来预防或识别这些错误。
首先,我们可以使软件开发过程更加精密,以防其中出现很多错误;其次,在每个阶段结束时可以引入一个独立的验证过程,在进入下一个阶段之前尽可能多地发现问题。
这种方法如图6-2所示。
举例来说,对外部规格说明的验证可以通过与前一个阶段的输出(对目标的叙述)进行比较,然后将任何发现的错误反馈到外部规格说明定义过程中去。
在第七阶段结束时,使用本书第3章讨论的代码检查和走查方法进行验证。
未经同意,严禁以任何形式拷贝 95 第6章更高级别的测试 最终用户 验证验证验证验证验证验证验证 需求目标外部规格说明系统设计程序结构设计模块接口规格说明代码 图6-2包含中间验证步骤的开发过程 第三个方法是对不同的开发阶段采用不同的测试方法。
也就是说,将每一个测试过程都重点针对一个特定的转换步骤,从而也针对一类具体的错误。
这种方法如图6-3所示。
测试周期是模仿软件开发周期建立起来的,换言之,我们应该能够在开发过程和测试过程之间建立起一对一的联系。
举例来说: •模块测试的目的是发现程序模块与其接口规格说明之间的不一致。
•功能测试的目的是为了证明程序未能符合其外部规格说明。
•系统测试的目的是为了证明软件产品与其初始目标不一致。
这种结构的好处是避免了没有效果的多余测试,并使我们不会遗漏掉大量的错 误类型。
举例来说,不能仅将系统测试定义为“对整个系统的测试”并且可能仅重 未经同意,严禁以任何形式拷贝 96 软件测试的艺术 复先前的测试,而是针对一种特定类型的错误(在将目标转换为外部规格说明时所犯的错误),并就开发过程中的特定类型的文档进行度量。
图6-3所示的更高级别的测试方法最适用于软件产品(作为合同的结果或面向广泛应用而编写的程序,与做试验用的或仅供作者本人使用的程序有所不同)。
不作为产品而编写的程序常常没有正规的需求和目标。
对于这些程序,功能测试可能就是惟一的更高级别的测试。
同时,对更高级别测试的需求是与程序的规模一同增长的。
这是由于在大型程序中,设计错误(在早期开发阶段所犯的错误)与编码错误之间的比率要比在小程序中的比率高很多。
最终用户 验证验证验证验证验证验证验证 需求目标外部规格说明系统设计程序结构设计模块接口规格说明代码 安装测试 验收测试系统测试功能测试集成测试模块测试 图6-3开发过程与测试过程的对应关系 未经同意,严禁以任何形式拷贝 97 第6章更高级别的测试 注意,图6-3所示的测试过程顺序并不定意味着严格的时间顺序。
举例来说,由于系统测试并非定义为“功能测试之后进行的测试类型”,而是定义为一种特定类型的测试,关注于具体类型的错误,因此它很有可能与其他测试过程在时间上发生部分重叠。
在本章中,我们将讨论功能测试、系统测试、验收测试和安装测试的过程。
在这里忽略了集成测试,因为集成测试往往并不作为一个独立的测试步骤,而且在进行增量模块测试时,它是模块测试的隐含部分。
我们将简要讨论这些测试过程,并且大多不提供范例,因为这些更高级别的测试所使用的特定测试技术是与具体的被测程序高度相关的。
举例来说,对操作系统进行的系统测试的特点(测试用例的类型、测试用例设计的方式、使用的测试工具)与对编译器、核反应堆控制程序或数据库应用程序所进行的系统测试的特点有很大不同。
本章的最后几节将会计论测试计划和测试组织等话题,以及决定何时终止测试这一重要问题。
6.1功能测试(FunctionTesting) 如图6-3所示,功能测试是一个试图发现程序与其外部规格说明之间存在不一致的过程。
外部规格说明是一份从最终用户的角度对程序行为的精确描述。
除了在小程序中的使用情况之外,功能测试通常是一项黑盒操作。
也就是说,要依赖早期的模块测试的过程来实现理想的白盒逻辑覆盖准则。
在进行功能测试时,需要对规格说明进行分析以获取测试用例集。
本书第4章所讨论的等价类划分方法、边界值分析方法、因果图分析方法和错误猜测方法尤其适合于功能测试。
实际上,第4章中的例子就是功能测试的范例。
对FORTRAN语言的DIMENSION语句、考试评分程序以及DISPLAY命令的描述实际上就是外部规格说明的例子(但是,请注意它们并不是完全现实的例子:例如真正的评分程序的外部规格说明应该包括对报告格式的准确描述)。
因此,在本节中不再列举有关功能测试的例子。
未经同意,严禁以任何形式拷贝 98 软件测试的艺术 本书第2章中的很多原则也特别适合于功能测试。
跟踪哪些功能暴露出的错误数量最多,这个信息作常重要,因为它告诉我们这些功能很可能还包含着大多数尚未发现的错误。
应记住对无效和未预想到的输入条件给予足够的重视。
回想一下,对预期结果的定义是测试用例的重要部分。
最后,应始终牢记功能测试的目的是为了暴露程序的错误以及与规格说明不一致之处,而不是为了证明程序符合其外部规格说明。
6.2系统测试(SystemTesting) 系统测试最容易被错误理解,也是最困难的测试过程。
系统测试并非是测试整个系统或程序功能的过程,因为有了功能测试,这样会显得多余。
如图6-3所示,系统测试有着特定的目的:将系统或程序与其初始目标进行比较。
给定这个目标之后,隐含两方面的含义:
1.系统测试并不局限于系统。
如果产品是一个程序,那么系统测试就是一个试图说明程序作为一个整体是如何不满足其目标的过程。

2.根据定义,如果产品没有一组书面的、可度量的目标,系统测试也就无法进行。
在寻找程序与其目标之间的不一致的过程中,应重点注意那些在设计外部规格说明的过程中所犯的转换错误。
系统测试因而成为一种关键的测试类型,因为就软件产品本身、所犯错误的数量及其严重性而言,开发周期的这个阶段是最易出错的。
这也暗示与功能测试的情况不同,外部规格说明不能作为获得系统测试用例的基础,否则就破坏了系统测试的目标。
然而另一方面,也不能利用目标文档本身来表示测试用例,因为根据定义,这些文档并不包含对程序外部接口的准确描述。
克服这一两难局面的方法是利用程序的用户文档或书面材料。
通过分析目标文档来设计系统测试,分析用户文档来阐明测试用例。
该方法能够产生两方面的作用,一是将程序与其目标和用户文档相比较,二是同时也将用户文档与程序目标相比较,如图6-4所示。
未经同意,严禁以任何形式拷贝 99 第6章更高级别的测试 图6-4系统测试 图6-4说明为什么系统测试是最困难的测试过程。
图中最左边的箭头表示将程序与其目标进行比较,是系统测试的核心目的,但是没有说明使用什么样的测试用例设计方法。
因为目标文档阐述了程序应该做什么、做到什么程度,却没有说明程序功能如何表现。
举例来说,本书第4章中定义的DISPLAY命令的目标如下: 该命令用来从终端查看主存储空间中的内容,其语法应与所有其他系统命令的语法相一致。
用户可以通过一个地址范围或者一地址加上一数值来定义空间范围。
该命令操作符应具有合理的默认值。
命令的输出可以分多行显示多个字(以十六进制形式),字与字之间以空格相隔。
每一行须包含该行第一个字的地址。
该命令是条“不太重要的”指令,意味着其在合理的系统负载下,应在两秒之内开始显示输出,输出各行之间不应有可觉察的延时。
命令处理器中发生的编程错误在最坏情况下可能导致该命令失效;而系统以及用户交互则不应受到影响,系统投入使用之后,命令处理器中包含的用户发现的错误不应超过一个。
未经同意,严禁以任何形式拷贝 100 软件测试的艺术 目标虽已阐明,但并没有确认生成测试用例集的方法,仅有一些含糊却有用的指南来指导如何编写测试用例,以试图证明程序与目标文档中每一条语句都存在着不一致性。
因此,系统测试采取了一种不同的测试用例设计方法,不是描述一项技术,我们讨论的是不同类型的系统测试用例。
由于没有一个方法,系统测试需要大量的创造性。
事实上,设计好的系统测试用例比设计系统或程序需要更多的创造性、智慧和经验。
这里我们将讨论15种测试用例,我们并不认为所有的15种类型都适用于任何程序,但是为了避免有所遗漏,设计测试用例时应考虑全部的15种类型。
6.2.1能力测试(FacilityTesting) 最明显的系统测试类型是判断目标文档提及的每一项能力(或功能,为了避免与功能测试发生混淆而不使用“功能”一词)是否都确实已经实现。
能力测试的过程是逐条语句地检查目标文档,当某条语句定义了一个“要做什么”(例如,“语法应该一致……”、“用户应当可以指定一个空间范围……”等),就判断程序是否满足。
此种类型的测试常常可以在不使用计算机的情况下进行;有时人工对目标和用户文档进行比较就足够了。
尽管如此,利用问题检查单将有助于在下一次进行测试时,确保人工检查的目标是相同的。
6.2.2容量测试(VolumeTesting) 第二类系统测试是使程序经受大容量数据的检验。
举例来说,编译器可能要编译规模非常庞大的源程序,连接编辑器可能需要处理一个包含上千模块的程序,电子电路模拟器可能要输入一个包含上千部件的电路,而操作系统的作业队列可能已经达到饱和的容量。
如果程序需要处理跨越不同卷的文件,则应产生足够的数据使程序从一个卷转换到另一个中。
换言之,容量测试的目的是为了证明程序不能处理目标文档中规定的数据容量。
由于容量测试显然需要大量的资源,鉴于对机器和工时的考虑,不可进行过多的容量测试。
当然,每个程序应该至少进行几次容量测试。
未经同意,严禁以任何形式拷贝 101 第6章更高级别的测试 6.2.3强度测试(StressTesting) 强度测试使程序承受高负载或强度的检验。
这不应和容量测试发生混淆;所谓高强度是指在很短的时间间隔内达到的数据或操作的数量峰值。
类似的情况是测试一名打字员。
容量测试是判断打字员能否处理大篇幅的稿子,而强度测试则是判断打字员能否达到每分钟50个单词的速度。
由于强度测试涉及时间因素,因此,它不适用于很多程序,如编译器或批处理工资程序。
然而,强度测试适用于在可变负载下运行的程序,以及交互式程序、实时程序和过程控制程序。
假如某个空中交通控制系统要求在其区域内最多可跟踪200架飞机,则可以通过模拟200架飞机存在的情况来对其进行强度测试。
由于在客观上无法避免第201架飞机进入该区域,因此需要进一步的强度测试,以考察系统对这个不速之客的反应。
附加的强度测试则会模拟大量飞机同时进入该区域的情况。
如果操作系统要求支持最多15个多道程序的作业,则可尝试同时运行15个作业对其进行强度测试。
可以让学员强行打左舵、后拉节流阀、放下襟翼、抬起机头、放下起落架、打开着陆灯并向左转弯等所有这些操作同时进行,观察系统如何反应,从而对飞行员训练模拟器进行强度测试(这个测试用例可能需要一个长着四只手的飞行员,或者现实一点,需要飞行座舱里有两个测试专家)。
可以通过让所有被监视的过程同时产生信号,来对过程控制系统进行强度测试。
当对电话交换系统进行强度测试时,可以让大量电话同时打入该系统。
基于web的应用程序是最常按受强度测试的软件之
一。
在这里,我们需要确信的是应用程序及硬件能够处理一定容量的并发用户。
读者可能会争辩说,也许有数百万人在同一时刻访问站点,但这是不现实的。
我们需要弄清用户群,然后设计一个强度测试,体现出可能访问站点的最大人群的情况。
本书第9章将提供关于测试基于Web应用程序的更多信息。
虽然有很多强度测试体现的是程序在运行过程中可能会遇到的情况,然而也有另一些强度测试确实体现了“不可能发生”的情况,但这并不意味这些测试是无用的。
如果在这些不可能发生的情况中检查出了错误,那么这项测试就是有价值的, 未经同意,严禁以任何形式拷贝 102 软件测试的艺术 因为同样的错误也可能发生在现实的、强度稍低的环境中。
6.2.4易用性测试(UsabilityTesting) 系统测试的另一个重要类型是试图发现人为因素或易用性的问题。
当本书的第一版出版时,计算机行业并不太注意研究和定义编程系统中良好的人为因素问题。
今天的软件系统,尤其是那些为广大商业市场而设计的软件,通常都进行了广泛的人为因素的研究,而现在的软件自然也受益于过去的成千上万的程序和系统。
然而,对人为因素的分析依然是一项极为主观的事情。
在以下的清单中,我们列举了需要测试的一些问题:
1.每个用户界面是否都根据最终用户的智力、教育背景和环境要求而进行了调整?
2.程序的输出是否有意义、不模糊且没有计算机的杂乱信息?
3.错误诊断(如错误信息)是否直接,用户是否需要有计算机学科的博士学 位才能理解它门?举例来说,程序是否产生了诸如“IEK022AOPENERRORONFILE'SYSIN'ABENDCODE=102?
”此类的信息?象这样的信息在二十世纪
七、八十年代的软件系统中并不鲜见。
今天的面向大众销售的系统在这方面有了改进,但我们仍然会遇到诸如“出现一个未知错误”或“程序遇到了一个错误,必须重新启动”这样无用的信息。
自己编写的程序是由自己控制的,不应加入这些无用的信息。
即使我们并不开发软件,如果在测试小组中工作,那么可以推动人机界面这个领域的改进。

4.整体的用户界面是否在语法、惯例、语义、格式、风格和缩写方面展现出了相当程度的概念完整性,基本的一致性和统一性?
5.在准确性极为重要的环境里,如网上银行系统,输入中是否有足够的冗余信息?举例来说,该系统可能会要求输入账号、用户名和PIN(个人识别号)来验证访问账户信息的是合法用户。

6.系统是否包含过多或不太可能用到的选项?现代软件的一个趋势是,仅向用户提供那些基于软件测试和设计考虑而确定出的最有可能使用的菜单选项。
一个设计良好的软件可以向用户学习,并开始向不同的用户展示其经常访问的菜单项。
即使已经有了这样智能化的菜单系统,仍需要设计成功 未经同意,严禁以任何形式拷贝 103 第6章更高级别的测试 的软件,使得对不同选项的访问合乎逻辑、符合直觉。

7.对于所有的输入,系统是否返回了某些类型的即时确认信息?举例来说, 在点击鼠标进行输入的环境里,被选项可以变换颜色,或者某个按钮对象可以显示凹进或凸起的状态。
如果要让用户从列表中选择,那么当用户做出选择后被选序号应显示在屏幕上。
还有,如果被选的操作需要一些运行时间(如果软件正在访问一个远程的系统,情况常会如此),那么应显示一条信息通知用户当前正在做什么。

8.程序是否易于使用?举例来说,如果输入是区分大小字符的,这一点对用户来说是否清楚?此外,如果程序要求浏览一系列的菜单或操作,那么返回到主菜单的方法是否清楚?用户是否可以很容易浏览到上一层或下一层? 6.2.5安全性测试(SecurityTesting) 由于社会对个人隐私的日益关注,许多软件都有特别的安全性目标。
安全性测试是设计测试用例来突破程序安全检查的过程。
举例来说,我们可以设计测试用例来规避操作系统的内存保护机制,破坏数据库管理系统的数据安全机制。
设计此种测试用例的方法之一是研究类似系统中已知的安全问题,然后生成测试用例,尽量暴露被测系统存在相似问题。
例如,在杂志,聊天室和新闻组中发布的资料,经常包含有操作系统或其他软件系统的已知错误。
通过在与被测软件提供相似服务的现有系统中搜寻安全漏洞,可以设计测试用例来判断软件是否受到类似问题的困扰。
基于web的应用程序常常比绝大多数程序所需的安全测试级别更高。
对于电子商务网站尤其如此。
尽管已经有了足够多的技术(例如密码学)允许客户在因特网上安全地完成交易,但不能单纯依赖技术的应用来确保安全。
除此之外,我们必须向客户群证明软件是安全的,否则就会有失去客户的风险。
另外,本书第9章提供了更多的有关基于因特网的应用程序的安全性测试的资料。
6.2.6性能测试(PerformanceTesting) 很多软件都有特定的性能或效率目标,这终特性描述为在特定负载和配置环境下程序的响应时间和吞吐率。
再一次强调,由于系统测试的目的是为了证明程序不 未经同意,严禁以任何形式拷贝 104 软件测试的艺术 能实现其目标,因此应设计测试用例来说明程序不能满足其性能目标。
6.2.7存储测试(StorageTesting) 类似地,软件偶尔会有存储目标,举例来说,可能描述了程序使用的内存和辅存的容量,以及临时文件或溢出文件的大小。
应设计测试用例来证明这些存储目标没有得到满足。
6.2.8配置测试(ConfigurationTesting) 诸如操作系统,数据库管理系统和信息交换系统等软件都支持多种硬件配置,包括不同类型和数量的I/O设备和通信线路,或不同的存储容量。
通常可能的配置数量非常之大,以至于测试无法面面俱到,但是至少应该使用每一种类型的设备,以最大和最小的配置来测试程序。
如果软件本身的配置可忽略掉某些程序组件,或可运行在不同的计算机上,那么该软件所有可能的配置都应测试到。
如今的很多软件都设计成可运行在多种操作系统下,因此如果测试此类程序,应该在该程序面向的所有操作系统环境中对其进行测试。
对设计在Web浏览器里运行的程序,需要特别的注意,因为Web浏览器的种类繁多,并不是所有浏览器都按同样方式运行。
除此之外,即使是同一种Web浏览器,在不同的操作系统之下,运行方式也会不同。
6.2.9兼容性/配置/转换测试(Compatibility/Configuration/ConversionTesting) 大多数开发的软件都并不是全新的,常常是为了替换某些不完善的系统。
这样的软件往往有着特定的自标,涉及与现有系统的兼容以及从现有系统的转换过程。
再次强调,在针对这些目标测试程序时、测试用例的目的是证明兼容性目标未被满足,转换过程并未生效。
在将数据从一个系统转移到另一个系统时,应尽力发现错误。
升级数据库管理系统就是一个例子。
需要确定现有的数据安置到了新的系统中。
有很多不同的方法测试这个过程;但这些方法都高度依赖于所使用的数据库系统。
未经同意,严禁以任何形式拷贝 105 第6章更高级别的测试 6.2.10安装测试(InstallabilityTesting) 有些类型的软件系统安装过程非常复杂,测试安装过程是系统测试中的一个重要部分。
对于包含在软件包中的自动安装系统而言,这尤其重要。
安装程序如果出现故障,会影响用户对软件的成功体验。
用户的第一次体验来自于安装软件的过程。
如果这个过程进行得很槽糕,用户或顾客就要么寻找其他的产品,要么对软件的有效性不抱太大信心。
6.2.11可靠性测试(ReliabilityTesting) 当然,所有类型的测试都是为了提高软件的可靠性,但是如果软件的目标中包含了对可靠性的特别描述,就必须设计专门的可靠性测试。
测试可靠性目标可能很困难。
举例来说,诸如公司广域网(WAN)或因特网服务供应商(ISP)等现代在线系统在整个运行期间,正常运行时间应占99.97%。
我们现在还不太可能花上数月甚至数年的时间来测试这个目标。
今天的关键软件系统的可靠性标准甚至更高,而现今的硬件可以令人信服地保障这个目标的实现。
但如果软件或系统有更为适中的平均故障间隔时间(MTBF)目标或合理的(以测试而言)功能错误目标,就有可能对其进行测试。
例如,MTBF值不超过20个小时,或者系统目标是程序在投入使用之后暴露的不同错误的数量不得超过12个,那么就可以进行测试,特别是使用了统计的、程序验证的或基于模型的测试方法之后。
这些方法都超出了本书的范围,但有些技术文献(网上或其他方面的)对这个领域提供了充分指导。
举例来说,如果读者对软件测试的这个领域感兴趣,可以研究归纳断言的概念。
这种方法的目的是设计出有关被测软件的一系列定理,作为判断软件中不存在错误 的依据。
这种方法首先要对程序的输入条件及其正确结果编写断言。
断言用形式逻 辑的符号表示,通常是一阶谓词演算。
然后需要确定程序中的每个循环,对于每
个循环都写一个断言,描述出在循环中任意点都不变的(总为真)条件。
程序现在 已经被划分为固定数量的固定长度的路径(在成对的断言中的全部所有路径)。
对 于每一条路径,取中间程序语句的语义来修改断言,最终到达路径的终点。
此时在 路径的终点处存在着两条断言:原先的断言以及从路径的另一个终点处断言引申出 未经同意,严禁以任何形式拷贝 106 软件测试的艺术 的断言。
然后写出一条定理,说明原先的断言隐含着引申出的断言,并试图证明这个定理。
如果能够证明该定理,就可以认为该程序不存在错误——只要程序最终能够结束,需要单独的证明来说明程序总会结束。
虽然此种类型的软件证明或预测听起来非常复杂,但可靠性测试及软件可靠性工程(SRE)的概念已经为我们所认识,并且对于那些必须维持非常高的正常运行时间的系统,其重要性日益增加。
为了说明这一点,请查看表6-l中某个系统为支持不同的正常运行时间的需要而每年必须达到的运行小时数。
这些数字能够说明对SRE的需求。
表6-1不同正常运行时间要求的小时数/年 正常运行时间的比例需求 要求的运行小时/年 100 8760.0 99.9 8751.2 98 8584.8 97 8497.2 96 8409.6 95 8322.0 6.2.12可恢复性测试(RecoveryTesting) 诸如操作系统、数据库管理系统和远程处理系统等软件通常都有可恢复性目标,说明系统如何从程序错误、硬件失效和数据错误中恢复过来。
系统测试的一个目标是证明这些恢复机制不能够正确发挥作用。
我们可以故意将程序错误置入某个系统中,判断系统是否可以从中恢复。
诸如内存校验错误或I/O设备错误等硬件错误也可以进行模拟。
而如通信线路中的嗓音或数据库中的无效指针等数据错误可以故意生成或模拟出来,以分析系统的反应。
这些系统的设计目标之一是使平均恢复时间(MTTR)最小。
系统宕机才通常会减少公司的收入。
我们的一个测试目标是证明系统不能满足MTTR的服务合同。
MTTR往往有上界和下界,所以测试用例应反映出这些界限。
6.2.13适用性测试(ServiceabilityTesting) 软件还可能有适用性或可维护性的目标。
所有的此类目标都必须测试到。
这些 未经同意,严禁以任何形式拷贝 107 第6章更高级别的测试 目标可能定义了系统提供的服务辅助功能,包括存储转存程序或诊断程序、调试明显问题的平均时间、维护过程以及内部业务文档的质量等。
6.2.14文档测试(DocumentationTesting) 如同我们在图6-4中所描述的那样,系统测试也需要检查用户文档的正确性。
完成此任务的主要方法是根据文档来确定系统测试用例的形式。
也就是说,一旦设计完成某个具体的测试情况,应该使用文档作为编写实际测试用例的指南。
同时,用户文档应成为审查的对象(类似于本书第3章中的代码审查的概念),检查其正确性和清晰性。
在文档中描述的任何范例应编成测试用例,并提交给程序。
6.2.15过程测试(ProcedureTesting) 最后,很多软件都是较大系统的组成部分,这些系统并不完全是自动化的,包含了很多人员操作过程。
在系统测试中,必须对所有已规定的人工过程,如系统操作员、数据库管理员或最终用户的操作过程进行测试。
举例来说,数据库管理员必须记录备份和恢复数据库系统的操作过程。
在可能的情况下,应由与数据库管理不相关的人来测试这些过程。
然而,公司必须为充分测试这些过程而提供所需的资源,这些资源通常包括硬件和额外的软件许可证。
6.2.16系统测试的执行 系统测试执行中一个最关键的考虑是决定由谁来进行测试。
我们从反面来回答这个问题:(l)不能由程序员来进行系统测试;
(2)在所有的测试阶段之中,这是惟一明确不能由负责该程序开发的机构来执行的测试。
第一点基于的事实是,执行系统测试的人思考问题的方式必须与最终用户相同,这意味着必须充分了解最终用户的态度和应用环境,以及程序的使用方式。
那么显然的是,如果可行的话,一位或多位最终用户是很好的执行测试的候选人。
但是,由于一般的最终用户都不具备执行很多前面所描述的测试类型的能力或专业技术,因此,理想的系统测试小组应由几位专业的系统测试专家(以执行系统测试作为职业)、一位或两位最终用户的代表、一位人类工程学工程师以及该程序主要的 未经同意,严禁以任何形式拷贝 108 软件测试的艺术 分析人或设计者所组成。
将原先的设计者包括进来并不违反先前的测试原则,即不提倡测试由自己编写的程序。
因为程序自构思以来已经历经人手,所以原先的设计者不会再受到心理束缚的影响,对程序的测试不会再触及该原则。
第二点基于的事实是,系统测试是一项“随心所欲,百无禁忌”的活动,而软件开发机构会受到心理束缚,有悖于此项活动。
而且大多数的开发机构最为关心的是让系统测试进行得尽可能顺利并按时完成,而不会尽力证明程序不能满足其目标。
系统测试至少应由很少(如果有的话)受开发机构左右的独立人群来执行。
也许最经济的执行系统测试的方式(所谓经济,是指花一定的成本发现最多的错误,或利用更少的费用发现相同数量的错误)是将测试分包给一个独立的公司来完成。
这一点将在本章的后面章节进一步讨论。
6.3验收测试(eptanceTesting) 让我们回到图6-3所示的开发过程的完整模型上来,可以看到验收测试是将程序与其最初的需求及最终用户当前的需要进行比较的过程。
这是一种不寻常的测试类型,因为该测试通常是由程序的客户或最终用户来进行,一般不认为是软件开发机构的职责。
对于软件按合同开发的情况,由订购方(用户)来进行验收测试,将程序的实际操作与原始合同进行对照。
如同其他类型的测试一样,验收测试最好的方法是设计测试用例,尽力证明程序没有满足合同要求。
假如这些测试用例都是不成功的,那么就可以接受该程序。
对于软件产品的情况,如计算机制造商的操作系统或编译器,或是软件公司的数据库管理系统,明智的用户首先会进行一次验收测试以判断产品是否满足其要求。
6.4安装测试(InstallationTesting) 图6-3描述的测试过程的剩余部分是安装测试。
安装测试在图6-3中的位置有些不寻常,它与所有其他测试过程不同。
与设计过程中的任何阶段都没有联系。
它的不寻常是由于其目的不是为了发现软件中的错误,而是为了发现在安装过程中出现的错误。
在安装软件系统期间会发生很多事件。
作为示例的简短列表包括了下列事件: 未经同意,严禁以任何形式拷贝 109 第6章更高级别的测试
1.用户必须选择大量的选项。

2.必须分配并加载文件和库。

3.必须进行有效的硬件配置。

4.软件可能要求网络联通,以便与其他软件连接。
安装测试应由生产软件系统的机构来设计,作为软件的一部分来发布,在系统安装完成之后进行。
除此之外,测试用例需要检查以确认已选的选项集合互不冲突,系统的所有部件全部存在,所有的文件已经创建并包含必需内容,硬件配置妥当等。
6.5测试的计划与控制 如果认为测试一个大型软件系统可能需要编写、执行和验证数万个测试用例、处理数千个模块、改正数千个错误、雇佣数百人花费一年甚至更长的时间工作,那么很明显我们在计划、监视和控制测试过程方面遇到了巨大的项目管理挑战。
事实上,这些问题非常繁杂,我们可以将整本书都用来讨论软件测试的管理问题。
本节的初衷是总结其中的一些问题。
正如第2章所提到的,在计划测试过程中最常出现的主要错误是默认为不会发现软件缺陷。
这个错误带来的显然结果是对计划投人的资源(人力、时间表及计算机时间)明显估计不足,这在计算机行业内是个声名狼藉的问题。
造成这个问题的原因是测试阶段处于开发周期的最后阶段,致使调整资源非常困难。
另外,可能是更重要的问题,即对软件测试的定义有误,因为很难看到对测试正确定义(测试的目的是发现错误)的人在假定找不到任何错误的情况下去计划一个测试。
与大多数项目的情况一样,计划是管理测试过程中至关重要的一环。
一个良好的测试计划应包括:
1.目标。
必须定义每个测试阶段的目标。

2.结束准则。
必须制定准则以规定每个测试阶段何时可以结束,该问题将在 下一节中讨论。

3.进度。
每个阶段都须有时间表。
应指出何时设计、编写和执行测试用例, 某些软件技术,如极限编程(在本书第8章中讨论)要求在程序编码开始之前就设计测试用例和单元测试。
未经同意,严禁以任何形式拷贝 110 软件测试的艺术
4.责任。
对于每一个阶段,应当确定谁来设计、编写和验证测试用例,谁来修改发现的软件错误。
由于在大型项目中讨论特定的测试结果是否代表错误时,有可能出现争端,因此还需要确定一名仲裁者。

5.测试用例库及标准。
在大型项目中,用于确定、编写以及存储测试用例的系统方法是必须的。

6.工具。
必须确定需要使用的测试工具,包括计划由谁来开发或采购、如何使用工具以及何时需要使用工具。

7.计算机时间。
计划每个测试阶段所需的计算机时间,包括用来编译应用程序的服务器(如果需要的话)、用来进行安装测试所需的桌面计算机、用来运行基于web应用程序的web服务器、联网的设备(如果需要的话)等等。

8.硬件配置。
如果需要特别的硬件配置或设备,则需要一份计划来描述该需求,该如何满足需求以及何时需要满足。

9.集成。
测试计划的一部分是定义程序如何组装在一起的方法(例如自顶向下的增量测试)。
一个系统如果包含大的子系统或程序,可按增量的方式组装在一起,例如可以使用自顶向下或自底向上的方法,但是这些构造块是程序或子系统,而不是模块。
如果是这种情况,就需要一个系统集成计划。
系统集成计划规定了系统集成的顺序、系统每个版本的功能以及编写“脚手架”代码以模拟不存在的部件的职责分工。
10.跟踪步骤。
必须跟踪测试进行中的方方面面,包括对错误易发模块的定位,以及有关进度、资源和结束准则的进展估计。
11.调试步骤。
必须制定上报已发现错误、跟踪错误修改进程以及将修改部分加入系统中去的机制。
调试计划中还应包括进度、责任分工、工具以及计算机时间/资源等。
12.回归测试。
回归测试在对程序作了功能改进或进行了修改之后进行,其目的是判断程序的改动是否引起了程序其他方面的退步。
回归测试通常重新执行测试用例中的某个子集。
回归测试很重要,因为对程序的改动和对错误的纠正要比原来的程序代码更容易出错(与报纸排版错误很相似,这些错误通常由于最后所做的编辑改动而引起的,而不是修改先前版本而引起的)。
回归测试计划规定了测试人员、测试方法和测试时间,它也是必须的。
未经同意,严禁以任何形式拷贝 111 第6章更高级别的测试 6.6测试结束准则 在软件测试过程中最难回答的一个问题,是判断何时终止测试,因为我们无法知道刚刚发现的错误是否是最后一个错误。
事实上,除了非常小的软件,期望所有的错误最终都能被发现是不切实际的。
在这种两难情况之下,而且基于经济条件也要求测试必须最终结束的事实,我们可能会产生疑惑,是极其武断地回答此问题呢,还是存在一些有用的终止准则?我们在实际中使用的典型的结束准则既无意义,也不能实现目标。
最常见的两个准则是:
1.用完了安排的测试时间后,测试使结束。

2.当执行完所有测试用例都未发现错误,测试便结束。
也就是说,当所有的 测试用例不成功时便结束。
第一条准则没有任何作用,因为我们可以完全什么都不做也可满足它。
它并不能衡量测试的质量。
第二条准则同样也是无用的,因为它也与测试用例的质量无关,而且也不能够实现测试目标,它下意识里鼓励我们编写发现错误可能性较低的测试用例。
正如本书第2章所述,人类是高度受目标驱使的。
如果一旦获悉所有的测试用例都不成功就完成了任务,那么人们就会下意识地朝这个目标编写测试用例,避开了有用的、高效的和具破坏性的测试用例。
有三类较为有用的结束准则。
第一类,但不是最佳的准则,根据的是特定的测试用例设计技术。
举例来说,我们会这样定义模块测试的结束准则: 测试用例来源于(l)满足多重条件覆盖准则,以及
(2)对模块接口规格说明进行边界值分析,产生的所有测试用例最终都是不成功的。
我们会在满足下列情况时规定功能测试结束: 测试用例来源于(l)因果图分析,
(2)边界值分析,以及
(3)错误猜测,产生的所有测试用例最终都是不成功的。
尽管这种类型的准则要优于前面提到的两条准则,但仍然存在三个问题。
首先, 未经同意,严禁以任何形式拷贝 112 软件测试的艺术 对于那些没有特定方法的测试阶段,如系统测试阶段,这类准则不起作用。

二,它要依赖于主观的度量,因为没有办法保证测试人员适当而又严格地使用特定的方法,如边界值分析方法。

三,不同于设置一个目标再让测试人员选择最佳的实现方法,它的做法正好相反,指定了测试用例设计的方法,却并不设定目标。
因此,这种类型的准则对于某些测试阶段有时很有效,但是只有在测试人员根据以往的经历,证明自己可以成功地使用测试用例设计方法时,这些准则方可适用。
第二类,也许也是最有价值的准则,是以确切的数量来描述结束测试的条件。
因为测试的目的是发现错误,为什么不将测试结束准则定为发现了某个既定数量的错误呢?举例来说,在对某个具体模块进行模块测试时,直到发现了三个错误才可以认为测试结束了。
也许系统测试的结束准则应该规定为发现并修改了70个错误,或测试实际进行了3个月,无论以后发生什么。
应该注意的是,虽然这种准则强化了软件测试的定义,但它也有两个问题,每一个都是可以解决的。
一个问题是判断如何获得要发现的错误数量。
得到这一数字需要进行下面几个预测:
1.预测出程序中错误的总数量。

2.预测这些错误中有多大比例可能通过测试而发现。

3.预测这些错误中有多少是由各个设计阶段产生的,以及在什么样的测试阶 段能够发现这些问题。
可以通过几种方法来大致预测错误的总数。
一种方法是利用以前程序的经验来预测出数字。
另外,还存在多种预测模型。
有些模块需要测试一段时间,记录下连续发现错误的间隔时间,然后将这些时间输入一个公式的参数中。
有些模块被置入一些已知但未公开的种子错误,测试一段时间后,检查被发现的种子错误与非种子错误的比例。
还有的模型则让两个独立的测试小组分别测试一段时间,然后检查各自找出的错误以及两个组找出的共同问题,再使用这些参数来预测错误的总数。
还有一种获得预计数字的粗略方法是使用行业范围内的平均值。
举例来说,在编码结束时(在进行代码走查或检查之前),一般程序中的错误数量大致是每100行语句中含4-8个错误。
上面列举的第二个预测(可以通过测试发现的错误比例)包含一定程度的随意猜测,考虑了程序的性质以及未发现的错误造成的后果。
未经同意,严禁以任何形式拷贝 113 第6章更高级别的测试 关于错误是如何及何时产生的,我们现在得到的信息还很少,因此第二个预测最为困难。
现有的数据表明,在大型程序中,大约有40%的错误是编码和逻辑设计错误,剩下的错误则产生于早期的设计阶段。
为了使用这个准则,需要根据手头的程序做出自己的预测。
这里有一个简单的例子。
假设我们要着手测试一个10,000行语句的程序,进行代码检查之后剩余的错误数量预计每100行语句5个错误,并且我们估计,作为测试的目标,要检查出98%的编码和逻辑设计错误,以及95%的早期设计错误。
这样,错误总数为500。
在这500个错误之中,我们假设200个错误是编码和逻辑设计错误,300个是设计缺陷。
因此,我们的目标是找出196个编码和逻辑设计错误以及285个设计错误。
表6-2显示了对何时可能发现错误的近似合理的预测。
表6-2对错误发现时期的推测 编码和逻辑设计错误 模块测试功能测试系统测试 总计 65%30%3%98% 设计错误 0%60%35%95% 如果我们计划进行4个月的功能测试、3个月的系统测试,可以建立如下3个结束准则:
1.当发现并修改了130个错误之后(估计的200个编码和逻辑设计错误中的65%),模块测试即告结束。

2.当发现并修改了240个错误之后(200个错误的30%加上300个错误的60%),或功能测试进行了4个月之后,无论后面发生什么,功能测试即告结束。
加上第二条的原因在于,如果我们很快发现了240个错误,那么就很有可能表明我们低估了错误的总数,因此,不应很早就结束功能测试。

3.当发现并修改了111个错误之后,或系统测试进行了3个月之后,无论以后发生什么,系统测试即告结束。
这类准则的另一个明显问题是过度地预测。
在上述例子中,如果在功能测试开始时剩余的错误数量少于240个会发生什么情况呢?根据这条准则,我们可能永远也不能结束功能测试。
未经同意,严禁以任何形式拷贝 114 软件测试的艺术 如果你仔细想一想,这里有一个奇怪的问题。
这个问题是错误的数量不够,程序的质量过高了。
我们可以不将其当成问题,因为这是个很多人都喜欢遇到的“问题”。
如果这个问题确实发生了,可以根据常识来解决它。
如果我们在4个月内没有发现240个错误,项目经理可以聘请一个局外人来分析测试用例,判断问题究竟是测试用例不足,还是测试用例很棒却没什么错误可发现。
第三类结束准则表面上似乎很容易,其中却涉及许多判断和直觉。
它需要我们在测试过程中记录每单位时间内发现的错误数量。
通过检查统计曲线的形状,常常可以决定究竟是继续该阶段的测试,还是结束它并开始下一测试阶段。
假设某个程序正在进行功能测试,对每周发现的错误数量都进行了记录。
如果第7周的曲线如图6-5的上部所示,那么即使发现的错误数量已经达到了结束准则,此时结束测试也会显得草率,因为,在第7周里我们似乎仍处于高峰(发现很多错误),此时最明智的决定(记住我们的目标是发现错误)是继续功能测试。
如有必要,设计额外的测试用例。
然而另一方面,如果曲线处于图6-5的下部,错误发现率明显下降,意味着我们已经“啃干净”了功能测试这块骨头,也许最佳的行动是结束功能测试并开始新的测试类型(也许是系统测试)。
当然,我们还必须考虑其他因素,比如错误发现率的降低是否是因为缺少计算机时间,或执行完了可用的测试用例。
未经同意,严禁以任何形式拷贝 115 第6章更高级别的测试 图6-5通过记录单位时间内发现的错误来预测测试的结束 图6-6显示了如果我们没有记录发现错误的数量,会发生什么情况。
该图显示了个非常大的软件系统的三个测试阶段。
一个显而易见的结论是,该项目在第6时段后不应转到别的阶段。
在第6时段,错误发现率还很高(对于测试人员而言,发现率越高越好)。
然而在这个时候转移到下一个阶段,导致了错误发现率的明显下降。
最佳的结束准则可能是上述三种类型的组合。
对于模块测试而言,特别是由于多数项目在此阶段都没有正式跟踪已发现的错误,最佳的结束准则可能是第一类。
我们应该要求使用一系列具体的测试用例设计方法。
而对于功能测试和系统测试而言,结束准则可能是发现了既定数量的错误,或用完了计划的时间,再出现什么都不管,但条件是错误分析与时间图的对比表明测试的效率已很低了。
未经同意,严禁以任何形式拷贝 116 软件测试的艺术 图6-6对某个大型项目测试过程的事后研究 6.7独立的测试机构 在本章的前面章节和第2章中,我们强调了软件机构应避免测试自己的软件,其中的原因在于,负责开发程序的机构难以客观地测试同一程序。
就公司的架构而言,测试部门应尽可能远离开发部门。
事实上,最理想的是测试机构不应是同一个公司的一部分,因为如果不是这样,测试机构仍然会受到与开发部门同样的管理压力的影响。
解决这个矛盾的一个方法是雇佣独立的公司进行软件测试。
这是个好主意,不管是系统的设计和使用单位开发的这个软件,还是第三方单位开发的这个软件。
这种做法常被提及的好处是提升了测试过程中的积极性、建立了与开发机构的良性竞争、避免了测试过程处于开发机构的管理控制之下,以及独立的测试机构带来的解决问题的专业知识。
未经同意,严禁以任何形式拷贝 117 第7章调试(DEBUGGING) 第7章调试(DEBUGGING) 简单地讲,调试是执行一次成功的测试之后所要进行的工作。
记住,所谓成功的测试,是指它可以证明程序没有实现预期的功能。
调试是一个包含两个步骤的过程,从执行了一个成功的测试用例、发现了一个问题之后开始。
第一步,确定程序中可疑错误的准确性质和位置;第二步,修改错误。
虽然调试对于程序测试来说非常必要、不可或缺,但它似乎是软件开发过程中最不受程序员欢迎的部分之
一。
其主要原因可能包括以下几点: •个人自尊会从中阻挠。
不管我们是否喜欢,调试都说明了程序员并不完美,要么在软件的设计,要么在程序编码时会犯错。
•热情耗尽。
在所有的软件开发活动中,调试是最耗费脑力的苦差事,况且,进行调试往往经受着来自机构或自身的巨大压力,必须尽可能快地改正问题。
•可能会迷失方向。
调试是艰苦的脑力工作,因为发现的错误实际上可能会出现在程序的任何语句中。
也就是说,如果不首先检查程序,我们就不能绝对地肯定在一个薪金管理程序出具的支票中出现的数字错误不是由某个子程序引起的,该子程序要求操作员将一个特定的表格传输给打印机。
让我们以诊断一个物理系统为例子作对比,如汽车。
假如汽车在爬坡时熄火了(症状),那么我们可能会迅速而有效地排除掉某些部件——调频/调幅收音机、速度表或汽车门锁——引起该故障的可能。
根据我们对汽车引擎的整体了解,该故障一定是发生在引擎上,我们甚至可以排除掉某些引擎部件,如水箱和滤油器。
•必须自力更生。
与其他软件开发活动相比,关于调试过程的研究、资料和正式的指南都比较少。
尽管本书是关于软件测试的,并不讨论调试,但这两个过程显然是相互联系的。
针对调试的两个步骤,即错误定位和错误修改,对错误进行定位可能解决了95%的 未经同意,严禁以任何形式拷贝 118 软件测试的艺术 问题。
因此,本章集中讨论错误的定位过程,当然是假定某个成功的测试用例已经发现了一个错误。
7.1暴力法调试(DebuggingbyBruteForce) 调试程序的最为普遍的模式是所谓的“暴力”方法。
这种方法之所以流行,是因为它不需要过多思考,是耗费脑力最少的方法,但同时也效率低下,通常来讲不是很成功。
暴力调试方法可至少被划分为三种类型:
1.利用内存信息输出来调试。

2.根据一般的“在程序中插入打印语句”建议来调试。

3.使用自动化的调试工具进行调试。
第一种类型,使用内存信息输出(通常使用十六进制或八进制格式粗略地显示所有的存储区域)是最缺乏效率的暴力调试方法,原因如下: •难以在内存区域写源程序中的变量之间建立对应关系。
•即使对下复杂程度较低的程序,内存信息输出也会产生数最非常庞大的数 据,其中的大多数都是与调试无关的。
•内存信息输出显示的是程序的静态快照,仅能显示出在某一个时刻程序的 状态;为了发现错误,还需要研究程序的动态状态(随时间的状态变化)。
•内存信息输出很少可以精确地在错误发生的地方产生,因此无法显示在错 误发生时程序的状态。
错误发生到输出内存信息这段时间之内程序执行的活动,可能会掩盖掉发现错误所需的线素。
•通过分析输出的内存信息来发现问题的方法并不大多(因此很名程序员都是密切注视,急切地渴望着错误能神奇地从内存信息输出中自行暴露出来)。
第二种类型,在失效的程序中插入输出变量值的语句,这种做法也不具有很强的优势。
它可能比内存信息输出要好一些,因为可以显示程序的动态状态,让我们检查的信息可以相对容易地与源程序联系起来。
但是这种方法同样也有很多缺点: •它不是鼓励我们去思考程序中的问题,而主要是一种碰运气的方法。
未经同意,严禁以任何形式拷贝 119 第7章调试(DEBUGGING) •它所产生的需要分析的数据量非常庞大。
•它要求我们修改程序,这些修改可能会掩盖掉错误、改变关键的时序关系, 或者会引入新的错误。
•它可能对小型程序有效,但如果应用到大型程序,成本就相当高。
况且对 于某些类型的程序,如操作系统或过程控制软件,这种办法甚至无法使用。
第三种类型,自动化调试工具的工作机制类似于在程序中插入打印语句,但是并不修改程序本身。
可以使用编程语言的调试功能,或使用特殊的交互式调试工具来分析程序的动态状态。
可能会用到的典型的语言功能有:产生可打印的语句执行轨迹的机制、子程序调用以及/或者对特定变量的修改等。
调试工具的一个共同的功能是可以设置断点,使程序在执行到某条特定语句或改动了某个特定变量的值时暂停执行,然后程序员就可以检查程序的当前状态。
同样,这种方法也主要是在碰运气,常常会生成数量过于庞大的无关数据。
这些暴力调试方法的主要问题在于:它们都忽略了思考的过程。
我们可以在调试程序和侦破谋杀案之间找出相似点来。
实际上,在几平所有的谋杀悬念小说中,谜案都是通过仔细分析线索,将表面上不重要的细节全联结起来而最终侦破的。
这不是一个使用蛮力的方法,要使用蛮力的是寻觅障碍物或搜寻财宝。
还有些证据表明,无论调试小组成员是富有经验的程序员还是学生,肯动脑筋而不是依赖别人帮助的人能够更快、更准确地发现程序错误。
因此,我们建议仅在下列情况下使用暴力调试方法:(l)其他的方法都失败了:
(2)作为我们下面将会讨论的思考过程的补充,而不是替代方法。
7.2归纳法调试(DebuggingbyInduction) 很显然,认真的思考能够发现大部分错误,甚至不需要调试人员使用调试工具.归纳是一种特殊的思考过程,可以从细节转到全局,也就是从线索(即错误的症状,可能是一个或多个测试用例的结果)出发,寻找线索之间的联系。
归纳的过程如图7-1所示。
未经同意,严禁以任何形式拷贝 120 软件测试的艺术 确定相关数据 组织数据 研究数据间联系 不能构造假设 能 证明假设不能 能 修改错误 图7-1使用归纳法的调试过程 归纳调试的步骤如下:
1.确定相关数据。
调试人员犯的一个主要错误是未能将所有可用的数据或症 状都考虑进去。
第一步是列举出所有知道的程序执行的正确和不正确之处, 这些不正确之处即是症状,让我们相信确实存在错误。
那些相似却不相同、 且未引起症状出现的测试用例提供了额外的有价值的线索。

2.

组织数据。
记住,归纳意味着从特殊到一般,因此第二步是组织这些相关 数据,以便观察线索间的模式,尤其重要的是要找到矛盾、事件,比如仅 当客户的保险金账户收支不太平衡时出现的错误。
我们可以采用图7-2所 示的表格来组织现有的数据。
“是什么”框列举的是总体的症状,“在何处” 框描述了这些症状出现的地方,“多大程度”框描述了这些症状的范围和重 要性。
注意“是”和“否’列,它们所描述的矛盾之处最终可能会导致对 错误的假设。
?
Is What Where When Towhatnext Isnot 图7-2组织线索的一种方法
3.做出假设。
下一步是研究线索之间的联系,利用线索结构里可能的模式做 出一个或多个关于错误原因的假设。
如果还无法做出推测,就需要更多的 未经同意,严禁以任何形式拷贝 121 第7章调试(DEBUGGING) 数据。
如果可能有多个假设存在,首先选择最有能的一个。

4.证明假设。
考虑到调试在进行时所承受的压力,这个时期最主要的错误是 忽略了这个阶段,直接跳到结论去改正问题。
但是在继续下一步之前,证明这些假设的合理性是非常重要的。
如果忽略了这一步,可能接下去只修改了问题症状,而没解决问题本身。
应将假设与其最初的线索或数据相比较,以此来证明假设的合理性,确定这些假设可以完全解释这些线索的存在。
如果无法解释,要么这些假设是无效的或不完整的,要么还有更多的错误存在。
举一个简单的例子、假设在第4章描述的考试评分软件报告了一个明显的错 误。
错误是在某些但不是所有情况下,中间值似乎不正确。
在某个特殊的测试用例 中,有51名学生被评分。
正确打印出宋的平均分数为73.2,但打印出的中间值是 26分,而不是预期的82分。
经过对该测试用例及其他一些测试用例结果的检查, 线索按图7-3所示的形式进行组织。
?
What Where Is报告3中显示的中间值不正确 仅在报告3中出现 When 当测试学生为51时发生 Towhatnext 显示的中间值为26。
当学生数量为1时也同样发生,显示的中间值。
Isnot计算平均值或标准偏差时出现在其它报告中出现。
学生成绩的计算似乎正确在测试学生数量为2和200时未发生 图7-3组织线索的例子 下一步是通过寻找模式和矛盾之处,做出关于该错误的假设。
我们看到的一个矛盾是这个错误似乎出现在学生人数为奇数的测试用例中,这也许是个巧合,但看来很重要,因为我们要根据学生人数为奇数或偶数而不同地计算中间值。
还有一个奇怪的模式:在些测试用例中,计算出来的中间值总是小于或等于学生的人数(26小等于51,l小等于1)。
这时,一个可能的方法是再重新运行一次学生人数为51名的测试用例,给学生打与以前不同的分数,看一下是如何影响中间依的计算的。
如果中间值仍然是26,那么“否——多大程度”框可以填上“中间值似乎与实际分数无关”。
尽管这个结果提供了一条有价值的线索,但即使没有它,我们可能已经 未经同意,严禁以任何形式拷贝 122 软件测试的艺术 能够猜出这个错误来。
从现有数据计算出的中间值似乎等于学生人数的一半,经过四舍五入后得到最接近的一个整数。
换句话说,如果将分数设想为存储在一个分类表里,该程序打印的是中间学生的人数而不是其成绩。
因此,我们有了一个关于该错误准确性质的坚定的假设。
下一步就是通过检查代码或执行一些附加的测试用例来证明这个假设。
7.3演绎法调试(DebuggingbyDeduction) 演绎的过程是从一些普遍的理论或前提出发,使用排除和精炼的过程,达到一个结论(错误的位置),参见图7-
4。
列出可能的原因 使用排除法 都被排除 提炼剩下的假设 收集更多数据 证明剩下能的假设 不能 修改错误 图7-4使用演绎法的调试过程 举个谋杀犯的例子,与归纳过程相反,首先从一系列嫌疑人入手,通过排除(花 匠有当时不在现场的合理证词)和提炼(罪犯可能是红色头发)的过程,判断出管 家可能犯了罪。
演绎的步骤如下:
1.

列举出所有可能的原因或假设.第一步是建立一份所有想象得到的错误线 索的清单,线索不需要有完整的解释,它们纯粹是一些推测,帮助我们组 织和分析现有的数据。

2.利用数据排除可能的原因。
详细检查所有的数据,尤其寻找存在矛盾的地 方(图7-2可以用在此处),然后尽量排除所有可能的原因,仅留下一条, 如果所有的原因都排除掉了,需要增加额外的测试用例,得到更多的数据 来设计新的推测。
如果剩下的原因多于一个,那么首先选择最有可能的原 因,即主要假设。

3.

标签: #好友 #后缀名 #视频文件 #格式文件 #程序 #怎么看 #压缩文件 #格式文件