通常情况下,一个软件模型说明的内容主要包括,在测试过程中你应该考虑到哪些问题,如何对测试进行计划,测试要达到什么目标,什么时候开始,在测试中你要用到哪些信息资源。一个好的模型可以引导你对问题进行思考,而不好的模型则只能使你误入歧途。
这里我要宣称的是,目前的大多数软件测试模型都是不好的模型。这是因为这些测试模型仅仅是软件开发模型的一些装饰和补充而已。
人们一直在苦苦寻找软件开发的模型,在创建了新的模型后,就把测试作为一个阶段放在模型的后面部分。因此测试总被作为一种事后行为,测试总是被开发所驱动。总的来说,我们是在检测他们的完成品。但是,作为事后处理的测试,其驱动方式是不正确的。实际上它显而易见地和开发过程中各种行为之间有关,测试没有起到应有的平衡作用。这样的测试只是检测了开发人员做了什么,而并没有检测到他们是否按照规则做了什么,这样的做法割裂了本该紧密联系的行为,剩下的只有那些匆忙而草率的想法所带来的伤害。
而这样做的结果就是效果很差的、效率很低的测试。效果很差的测试将导致很多bug没有被发现,而效率很低的测试所浪费的是成本。
在本文中,我要做2件事,其一,我要否定一个不好的模型,即V模型。我希望通过论述来表明,“单元测试”和“集成测试”这2个词汇可以从我们的词汇表中取消了。其二,我将描述一个更好的模型。不过首先我认为,要真正拥有一个充分合理的模型还为时尚早。我仅仅是描述了一些新模型应该符合的重要的要求。这些要求将在本文末尾处列举。
V模型有什么问题呢?
在本文中我要把V模型作为不好的模型的典型来进行分析。我选择V模型作为分析的典型是因为V模型是最广为人知的测试模型。
最典型的V模型版本一般会在其开始部分对软件开发过程进行描述,如下图所示:
(图1--V模型的各级开发阶段)
这是古老的瀑布模型。作为开发模型,它有很多问题,不过这里不作讨论。尽管它的各种状态是我们接着要讨论的大家最熟悉的V模型的基础。我的批评意见同时也针对其它的装饰在一些更好的开发模型之上的测试模型,例如螺旋模型[Boehm88]。
在V模型中,测试过程被加在开发过程的后半部分,如下图所示:
(图2--V模型示意图)
单元测试所检测的是,代码的开发是否符合详细设计的要求。集成测试所检测的是,此前测试过的各组成部分是否能完好地结合到一起。系统测试所检测的是,已集成在一起的产品是否符合系统规格说明书的要求。而验收测试则检测产品是否符合最终用户的需求。
对于测试设计,显而易见的是,V模型的用户往往会把执行测试与测试设计分开对待。在开发文档准备就绪后,就可以开始进行相关的测试设计。如下图所示,相应的测试设计覆盖在了相关的开发过程之上:
(图3--将测试设计覆盖了开发过程后的V模型)
V模型有着很吸引人的对称外形,并且把很多人都带入了歧途。本文将集中讨论它在单元测试和集成测试中引起的问题。
为了说明的方便,这里专门制作了以下图片,图中包括一个单独的单元,以及一个单元组,我称之为子系统(subsystem)。
(图4--一个假想的子系统)
对于一个单元应该多大才最为合适的问题,已经有过很多的讨论,究竟一个单元仅仅是一个函数,一个类,还是相关的类的集合?这些讨论并不影响我在这里所要阐述的观点。我们权且认为一个单元就是一个具有最小程度的代码块,开发人员可以对进行独立地讨论。
V模型认为人们首先应该对每一个单元进行测试。当子系统中所有的单元都已经测试完毕,它们将被集中到一起进行测试,以验证它们是否可以构成一个可运行的整体。
那么,如何针对单元进行测试呢?我们会查看在详细设计中对接口的定义,或者查看源代码,或者同时对两者进行查看,找出符合某些测试设计中的有关准则的输入数据来进行输入,然后检查结果,看其是否正确。由于各单元一般来说不能独立地运行,所以我们不得不另外设计桩模块(Stub)和驱动模块(Driver),如下图所示。
(图5:单元及其外部的驱动模块和桩模块)
图中的箭头代表了测试的执行轨迹。这就是大多数人所说的“单元测试”。我认为这样的方法有时候是一种不好的方法。
同样的输入也可以有同一子系统中的其它单元来提供,这样,其它的单元既扮演了桩模块,又扮演了驱动模块。如下图所示:
(图6--子系统内部各单元间的测试执行轨迹)
到底选择哪一种方法,这需要一种折衷和权衡。设计桩模块和驱动模块要付出多少代价?这些模块如何进行维护?子系统是否会由此而掩盖了一些故障?在整个子系统范围内进行排错的困难程度有多大?如果我们的测试直到集成测试时才真正开始,那么一些bug可能较晚才被发现。由此造成的代价同设计桩模块和驱动模块的代价如何比较?等等。
V模型没有去考虑这些问题,当单元开发完成后就执行单元测试,而当自系统被集中在一起后就执行集成测试,仅此而已。令我奇怪和沮丧的是,人们从不去做一些权衡,他们已经受制于他们的模型。
因此,一个有用的模型应该允许测试人员考虑节省并推迟测试的可能性。
一个测试,如果要发现一个特定的单元中的bug,最好是在该单元保持独立的情况下执行,并且在其外部辅以特定的桩模块和驱动模块。而另一种方法则是让它作为子系统的一部分来进行测试,该测试的设计主要是为了发现集成的问题。由于一个子系统本身也需要桩模块和驱动模块来模拟该子系统和其它子系统的联系,因此,单元测试和集成测试可能被推迟到至少整个系统已经部分集成的时候。在这种情况下,测试者可能通过产品的外部接口同时进行单元测试、集成测试和系统测试,同样的,其主要目的还是为了减少总体生命周期的成本,对测试成本和延期进行测试及由此造成延期发现bug的代价成本进行权衡。据此而言,“单元测试”、“集成测试”和“系统测试”的区别已经大大削弱了。其结果可参考下图:
(图7--新的方法:在部分阶段延迟进行单元测试和集成测试)
在上图右边的方块中,最好要改成为“执行某些适当的测试并得到相应的结果”。
图中的左边会怎样?考虑一下系统测试设计,它的主要根据和信息来源是是规格说明。假设你知道有2个单元处在一个特定的子系统中,它们在运行时相互联系,并且要执行规格说明中的一个特定的声明。为什么不在该子系统被集成时立即对此规格说明中的声明进行测试,就象是在设计完成后立即开始测试的设计一样呢?如果该声明的执行和子系统外的子系统没有任何关系,为什么还要等到整个系统完成以后再测试呢?难道越早发现bug成本越低不对吗?
在上一张图片中,我们用了向上指的箭头(更有效,但在时间上有延迟)。这里还可以把箭头往下指(在时间上提前):
(图8--新的方法:在不同阶段上提前进行测试设计)
在这种情况下,左边的方块中最好被标记为:“在当前信息条件和情况下可以做的任何测试设计”。这样,当测试设计得自于系统中某一个组件的描述时,模型必须允许这样的测试在组件被装配之前被执行。我必须承认我的图片非常难看,这些箭头指得到处都是,对此我有2点说明:
1. 我们所讨论的事情不是创造美,而是想要发现尽可能多的严重错误,同时尽可能地降低成本。
2. 难看的部分原因也是因为必须按照某些次序来执行的结果,亦即开发人员先提供系统描述文档,然后测试和这些文档进行关联。这些文档就象是坚实的老橡树,而测试设计则象是细细的枝条缠绕在树上。如果我们采用不同的原理来进行组织,图片可能就会变得好看些。但复杂性仍不可避免,因为我们要讨论的问题本身就很复杂。
V模型失败的原因是它把系统开发过程划分为具有固定边界的不同阶段,这使得人们很难跨过这些边界来采集测试所需要的信息。有些测试应该执行得更早些,有些测试则需要延后进行。而且,它也阻碍了你从系统描述的不同阶段中取得信息进行综合。例如,某些组织有时执行这样的做法,即对完成的工作进行签署。这样的规定也扩展到系统测试的设计。签署表示已经过评估,该测试设计工作已经完成,除非对应的设计文档改变,否则就不会被修订。如果同这些测试相关的信息后来被重新挖掘和认识,例如,架构设计表明有些测试是多余的,或者,详细设计表明有一个内部的边界可以和已存在的系统测试组合在一起进行测试的话,那么实际上还需要继续调整原来的系统测试设计。
因此,模型必须允许利用不同来源的综合信息进行个别的测试设计。另外,模型还应该允许在新的信息来源出现后重新进行测试的设计。
一个不同的模型
让我们来看本文的第二项内容,一个不同的模型。
很多时候人们把代码移交给其他人,并且说:“希望你能接受和喜欢它。”这不仅发生在将整个项目放在一张光盘中交给客户的时候,也发生在项目内部。
例如,一个小组对另一个小组说:“我们已经完成了为COMM库加入了对XML的支持。源代码现在已经放在master库中,可执行库则已经加入到集成与创建的环境中。XARG小组的工作已经没有什么阻碍了,随时去取吧。”
某个程序员检查了bug的修改并且发出邮件:“我已经修改了Bug列表中的那个Bug,很抱歉!”至此,早先受该问题影响的其它代码就可以继续处理了。
在这些情况下,人们要把代码移交给其它人,其中有可能会存在一些影响。测试人员需要干预这个过程。在移交之前,测试人员应执行这些代码,发现其中的bug(影响),并且提出问题:“你确实要提交这些吗?”由此,移交的内容可能会被延期,直到bug被修复好。
尽管你还要做其它的各种测试,这项测试仍然是很基本的测试工作。如果你没有做这样的测试,就不能算是合格的测试人员。
我们的测试模型必须包含这一重要的现实需要:针对代码移交的测试。由此,测试模型应提示进行针对每一次代码移交的测试。
就让我以支持XML的COMM库作为例子。这里存在着一个小组把代码移交给XARG小组以进行项目的余下部分。那么谁会遭受影响?
现在我们可以提出一些有趣的关于测试计划的问题了。最简单的可以做的事情是,在移交的时候立即执行XML支持的完整测试。(“完整”的含义是,为此设计尽可能多的测试)但也许一些XML特性并不是XARG小组所需要的,因此可以把它们放在合作伙伴版本封版(下图中的“Partner Release”)的测试中去。这意味着可以把一些XML相关测试放到稍后的移交过程中去。或者基于其它理由,例如在近阶段有其它的测试任务要执行。而XARG小组则要因XML中的bug修复而延迟一小段时间。
我们的测试计划所表示的进度可以通过在开发的时间线上进行注解的方式来表现,如下图所示:
(图9:添加在开发计划之上的测试计划)
我们应严格地围绕在XML支持的功能交接的时段里进行测试。测试设计和测试支持工作要早于测试执行。而另外的XML测试则要延迟到基于整个项目范围的“代码完成”(图中的“Code Complete”里程碑),或者要等到全部的子系统被集中在一起,而且整个产品为了行业会议而在经过稳定化处理后创建了版本(图中的“Partner Release”里程碑)。
显然,有两项内容没有包含在代码完成里程碑中:
我的观点是,测试计划是由很多困难的决定所组成,这些决定包括人员组织安排、机器资源配置、测试设计的时间定位、测试支持代码的数量、哪些测试要做自动化,等等。这些决定应根据单独的交接中的内容信息来作出。如果仅有一次交接,那么你可以更顺利一些。测试计划还应继续考虑以下问题:
1. 风险分析,谁会因此受到损害,以什么方式?
2. 选定一种测试途径来定位特定的风险。
3. 对测试设计和执行的周期和成本进行估计。
4. 在项目进度上的特定位置,将计划纳入执行的行动:
A. 开始对测试进行设计…
B. … 同时设计和创建一些支持测试的代码…
C. … 在全部测试完成以前就执行部分的测试,因为可能存在不只一次的交接,在每一次交接的测试规划中,可能存在一些潜在的复杂的相互影响。工作安排不得不进行一些调整以达到相互间的平衡。测试支持代码和工具需要在各项任务中得到共享。你还必须考虑到在什么程度上让那些为早先的交接所设计的测试在以后重新执行,等等。
这看起来很复杂。看上去似乎有太多的内容需要跟踪,而且太多的内容可能被忽略。也许你认为我是在要求你要对每一次交接来执行IEEE 829 [IEEE98]中关于测试计划的要求,然后把它们合并为一份贯穿整个项目的针对交接进行测试的测试计划。
是,也不是。思考问题总是要占用时间的。太多的计划可能会减少结果的产出。在有些时候,你需要做的是停止计划并开始行动。例如,你无法思考并描述每一个bug修复,尽管bug修复也是一种交接。
但是bug修复是实际工作中现实存在的问题。总体项目计划中应该包含bug修复。需要强调的是,你应该有一个默认的bug修改处理的标准过程,该过程应包括运行于每一个提交的bug修复的验证过程。你还需要努力地去思考问题。很多时候,各项验证是被放在一起同时进行并完成的。
比较现实地来说,一个模型应该允许一些机械式行为,例如,“不管是哪一个X类型的交接,都要执行下列操作”。同时我们鼓励对特定的交接执行刚刚够的检查,对于风险越小的交接,就越可以采用机械式的测试行为。
一个明确考虑到基本的测试现实的模型肯定会比忽略这些现实或者把你的工作复杂性完全抽象化的模型做得更好。文档则是另一个例子。
我还没有提到需求及规格说明书,或者设计文档。某个交接中产生的一系列变化会引起很多争议。这些文档所扮演的角色是什么呢?它们常常是这么被使用的:
(图10:测试中对开发文档的利用)
文档可以指导你在一个交接变化时如何作出反应。如果你有一份很好的需求文档,它可能是产品所解决的问题的描述,尽管也许不是很直接。它可以帮助你对风险进行分析。一份好的规格说明应对系统的行为进行描述。这将帮助你把测试方法转化为具体的测试。一份好的架构设计则可以帮助你理解变化可能引起的几种不同的情况:系统的其它部分会受到怎样的影响?什么测试需要再次进行?
我并不是经常能看到好的文档。需求文档常常象是市场销售用的系统特性列表。规格说明书有时就象是在代码完成后提交的用户手册文件。而设计文档经常不存在。
好了,通过针对交接所引起的变化的集中讨论,我已把测试过程和软件开发过程相对地分离开了。如果文档中关于“XML支持功能加入到COMM”的描述很薄弱的话,我会尽自己所知来进行尽可能好的测试设计。然后,假如在项目后期,XML相关的用户文档出来了,我就可以对后来再次提交的交接增加相关的测试。假如市场需求改变了,她们经常会这么做,我还会在此后增加或者去除一些测试。所有这一切看起来是这样的:
(图11--在文档不完整的条件下进行测试,并在后期补充测试)
这样,虽然项目文档总是不到位,而且经常延迟提交,测试的效果也因此常常被降低,我们还是要避免测试受到项目文档的制约。
头脑灵活的测试人员并不过于相信文档。毕竟,总是人在犯错误。那么,难道不是人在写这些文档吗?
由于“正式的”文档是很薄弱的,测试模型必须明确地鼓励在测试过程中使用项目文档之外的各种不同信息来源。
测试人员必须和程序员、用户、市场人员、技术作者以及任何的可能为实现更好测试提供线索的人进行交流。测试人员还应该努力把自己沉浸在某些技术所构建的氛围中。例如,我希望测试人员在做XML测试工作时常去访问W3组织的XML地址(http://www.w3.org/XML/)以及其它XML站点、邮件列表,甚至包括比较特别的 如Dave Winer的DaveNet/脚本新闻(http://www.scripting.com/)。这些资源并不是所谓的“辅助通道”,而是可以被列入计划和进度日程的资源。
另外,所执行的测试本身也是一种有用的信息的资源。好的测试人员会仔细阅读bug报告,因为这些报告讲授了系统所存在的薄弱之处。特别地,这些报告还暗示了一些正式的架构设计所没有提供的架构上的策略。执行测试的行为应该产生一些新的测试想法。如果模型没有考虑到这些,那么它就是一个落后的模型。
因此,测试模型应该包含反馈的循环,让测试设计可以考虑到,在运行测试时还可以继续发现到更多的测试内容。
在我们的工作中,真正的复杂性来自于所有计划的执行都处于一个不确定的、容易忽略的环境里。代码并不是唯一在不断变化的东西。而计划日程也在改变。新的功能扩充会带来新的里程碑。某些功能会从当前版本中去除。在开发过程中,所有人--市场人员、开发人员和测试人员,都会逐渐对诸如“产品究竟提供什么”这样的问题有越来越清晰的了解。在这些情形下,我们怎么能说测试计划的第一个版本会是完全正确的呢?
因此,模型应该要求测试计划人制定明确的规定,对已交接的交接内容,新的交接,以及交接内容的变更进行负责。
总结
V模型有以下致命的缺陷,其它模型实际上也与此相似:
1. 忽略了这样的事实情况,即软件开发是由一系列的交接所组成,每一次交接内容都改变了前一次交接的行为。
2. 依赖于开发文档的存在,及文档的精确性、完整性,并且没有对时间进行限制。
3. 认定一种测试的设计是依据某一个单独的文档,而不包括根据其前后阶段的文档的修改而作相应修改。
4. 认定这些依赖于某个单独文档的测试一定要在一起执行。
我大致描述了一个替代模型,但还不够精细。它考虑到了代码的交接和里程碑。对测试成本控制作了以下明确描述:
测试设计的目标是定义好可能发现bug的测试输入,而测试执行的目标是以各种方式加入这些数据,并检验结果,由此来降低整个生命周期的成本。
我们的模型假设软件产品总是不完美的,开发过程中有很多变更,而且对产品的测试也是一个不断学习的过程。
过去,我很少考虑到模型。表面上我一直还在用V模型。虽然我按此制订计划,但我总是还要花费很多额外的精力和时间来考虑模型中没有提到的方面。换言之,模型造成了一些阻碍,因此我有必要对此进行研究。
对一个新的模型来说,对模型所提出的要求必须非常明确,这就象业务需求对产品开发非常重要一样。我希望自己对本文中所提倡的模型的要求的描述能够和V模型中的描述一样精确,并具有同样的指导意义。
理想的测试模型应该包括下列要求:
1. 使测试对项目中的每一次代码交接有所反应。
2. 要求测试计划人制定明确的规定,对已交接的交接内容,新的交接,以及交接内容的变更进行负责。
3. 在测试设计中,除了使用项目文档外,还应明确鼓励使用其它各种信息,这些信息有不同来源。
4. 事实上项目文档总是不到位,而且经常延迟提交,测试的效果也因此常常被降低。但我们还是要尽量避免测试受到项目文档的制约。
5. 允许根据多种来源提供的综合信息来设计一些独立的测试。
6. 让测试被重新设计,以新的信息形式进行表现。
7. 包含反馈的循环,让测试设计可以考虑到,在运行测试时还可以继续发现到更多的测试内容。
8. 让测试人员认识到,避免测试的延迟可以节省成本。
9. 在组件被组装到程序中去之前对组件的执行进行测试。
感谢
是James Bach最早让我认识到,在测试计划中应考虑到交接。Noel
Nyman和Johanna Rothman在一份草案中提供了一些有帮助的评论。Kamesh Pemmaraju和Carol Stollmeyer使我终于没有放弃对原理的辩解和阐述,不仅是在细节方面,也在实际生活中,以及每一个实际项目中。他们给了我很大的促进,使我去反复地思考问题。
参考
[Boehm88]
Barry W. Boehm, “A Spiral Model of Software Development and Enhancement,” IEEE Computer, 1988年5月
[IEEE98]
"IEEE Standard for Software Test Documentation," IEEE标准829-1998, 电子和电气工程师学会, 1998
[Marick98]
Brian Marick, “When Should a Test be Automated?” 国际质量周刊,1998年5月(ftp://ftp.rstcorp.com/pub/papers/automate.pdf)