程序测试的心理学和经济学

                程序测试的心理学和经济学

                    作者:    Glenford     J.Myers

                    译者:          lebk

软件测试是一种技术工作,但经济学和心理学方面的考虑也显得相当重要。

 

理想状态,我们希望能测试程序的每一方面。在大多数情况下,这是不可能的。甚至一个看起来简单的程序也可能有成百上千个输入输出的组合。创建每种可能情况的test cases是不实际的。完全测试一个复杂程序需要花掉大量时间和人力资源,以至在成本上不可行。

 

因此,软件测试者需要正确的态度(或者说“眼力”)来有效的测试软件。在许多情况下,测试者的态度也许比实际的方法更重要。在讨论具体的测试技术前,我们先讨论这类问题。

 

测试的心理学

不能有效进行测试的一个主要原因是:许多人错误的理解了“软件测试”的含义。他们可能认为:

     测试是为了表明错误不存在

     测试是为了显示其功能正确执行

     测试是为了建立对软件的信心

 

这些定义都本末倒置了。

 

当测试程序时,你希望能加入一些价值到里面。测试的价值体现在,软件质量或可靠性的提高。提高可靠性意味着找到和移除错误。

 

因此,不要只是测试一个程序表明他是工作的;而是,你要以假定程序含有错误为出发点(对于几乎任一程序的正确假定),然后测试程序以找出尽量多的错误。

 

所以,一个更恰当的定义是:

         测试是挑错的过程。

这虽然听起来有点像文字游戏,但这的确是一个重大区别。理解软件测试的真实含义能深远的影响一个人能取得多大成功。

 

人是高度目的性的,建立合适目标对心理有重要影响。如果我们的目标是表明程序无错,那我们潜意识里就向这个目标前进;也就是说,我们趋向于选择那些不太可能使程序失败的测试数据,如果目标是使程序失败,那我们的测试数据就更可能找到错误。后一种方法能增加更多价值到里面。

 

这个定义给与我们大量启示,许多分散在本书之中。例如,他意味着,测试是一个破坏过程,甚至是“虐待”的过程,这就是为什么许多人认为测试很困难。那可能违背了我们的初衷,学多人对于生活的看法都是建设性的,而非破坏性。许多人倾向于创造事物而非破坏它们。这个定义还暗含着怎么设计test cases(test data),和谁应该谁不该测试程序。

 

对“成功”和“失败”的分析,能提高我们对这个定义的理解一一一般来讲,项目经理利用他们来划分测试案例(test cases)。许多项目经理把一个没有找到错误的test case 归入 successful test run,而把找到错误的归入“unsuccessful”。

 

这又一次本末倒置了。“unsuccessful”表明一些不需要或失望的东西。我们认为,测试称得上成功的,当它能找到错误,并且这些错误能修复。或者这些测试最终表明不会有错误发生。唯一失败的情况是:这些测试没能恰当的检查软件。一般,测试如果没有找到错误被认为是失败的,因为程序不含错误基本上是不现实的。

 

找到错误的测试案例(test case)不应该归入“unsuccessful”,相反,这才是有价值的。失败的测试案例(test case 是那些仅表明程序能正确执行的。

 

我们做一个类比,当某人感到不舒服去见医生。如果这个医生做了一些检查,但并没找他不舒服的原因,我们不会把这些检查称做“successful”;这些检查是失败的,因为他花了病人的钱,但好无效果。此时,病人可能质疑他作为医生的能力。相反,当这些检查表明病人有消化溃疡“peptic ulcer”,这个检查被称做成功的,因为紧接着医生就能进行恰当的治疗。因此,医学看起来正确的认识了这两个词的含义。作为类比,在开始测试时,我们应当把程序当做病人看待。

 

“测试是表明错误不存在的过程”的错误是:这个目标对于实际的程序,甚至是很小的程序都是不可能实现的。

 

心理学研究表明当人面对不可行或不可能的任务时,表现糟糕。例如,当我们被要求15分钟解决纽约时报上的纵横字谜时,我们找不了几个。因为许多人10分钟之后就开始认为,这是不可能的。如果你被要求在4小时内完成,我们许多人在头十分钟就能找到更多的单词。把软件测试定义为“表明软件存在错误的过程”,使这种工作变成可行的,从而可能解决这种心理问题。

 

“测试是表明程序正确的做它该做的事”的问题是,一个程序做了它该做的事人仍可能含有错误。因为,一个程序没有做它该做的事的错误是明显的,但是如果它做了不该做的事仍然错误。考虑第一章的三角形问题。当我们表明这个程序在等边,等腰,三边不等的情况下正确执行,这个程序仍可能含有错误(例如认为123是不等边,或者000是等边三角形)。我们更可能找到后一类错误如果我们把测试当作找错的过程而非是“表明程序做了它该做的事”。

 

综上,软件测试更恰当的看法是找错的破坏性过程。成功的测试案例(test case)是那些能使程序失败的。当然,你最终的目的是通过表明程序做了它该做的,没做他不该做的来建立对程序的某种程度的信心。而达到这个目的的最好办法是仔细的查找错误。

 

当某人到你面前声称“我的程序是正确的”(没有错误)。要相信这种话的最好方法是,尽力反驳它,也就是要找到程序的错误而非仅仅找些测试数据表明程序无错。

 

测试的经济学

按照我们对程序测试的定义,下一个步骤是判断找出程序的所有错误是否可行。我们的回答是否定的,甚至小程序也是。一般找出所有错误是不可行的,经常是不可能的。这个基本问题暗示着测试的成本,假设测试者要测试程序和设计测试案例(test cases)。

 

考虑到测试成本,你应当在初期就建立起某种策略。两种最流行的策略是黑盒测试和白盒测试,这些将在下面两部分讨论。

 

黑盒测试

一个重要的测试策略是黑盒测试,或者被称做数据驱动,输入输出驱动测试。采用这种方法,把程序当作黑盒子看待。你的目的是完全不关心程序的内部行为和结构。取而代之的是,把注意力放在它是否符合规格。

 

这种方法中,测试数据完全来源于规格(也就是说,外全没有利用程序内部结构的知识)。

 

如果决定采用这种方法来找出程序的所有错误,唯一做法是穷举测试:把每一个可能的输入作为测试案例(test case)。为什么?例如设计测试三角形等边的测试案例(test cases),没有一种方法能保证在所有情况下它都正确。程序可能含有特殊的情况,如把384238423842当做不等边三角形处理。由于是黑盒测试,唯一能保证不会出现这种情况的方法是尝试每一种输入。

 

为了完全测试三角形程序,应当创建这种程序开发语言所允许的所有有效输入。这是个天文数字,但还没穷举;它可能含有错误:如把-345当作不等边三角形,2A2当作等腰三角形。为了找到这种类型的错误,你的测试数据不仅要包括有效的输入,还有所有可能的输入。因此,为了完全测试这个三角形程序,你要创建的实际上是无限的测试案例(test cases,当然,这是不可能的。

 

如果这听起来很复杂,完全测试一个大些的程序更困难。考虑用黑盒方法来完全测试一个C++编译器。你不仅要创建所有有效的C++程序(又一次,无限的),你还要创建所有无效的C++程序(无限的)来保证编译器能检测出它们的错误。还要测试编译器没做多余的事-例如,没有正确编译一个语法错误的程序。

 

当程序有内存,例如操作系统或数据库时,问题变的更复杂。例如,数据库应用的一个例子:航空定票系统,一笔交易(如查询,或定票)依赖于以前的交易。因此,除了处理单独的有效和无效的交易外,还有所有可能的交易序列。

 

这些讨论表明穷举测试是不可能的。这给我们两点提示:(1)不能通过测试来表明程序无错(2)测试的一个基本考虑是成本。因此,穷举测试不可能,剩下的问题是怎样通过有限的(测试案例)test cases来发现更多的错误。为了达到这个目的,我们要采取一些方法,例如通过查看程序来做某些合理的,但不是绝对的假设(例如,当三角形程序把222当作等边三角形,我们有理由认为对333它仍能正确判断)。这将在第四章:测试案例的设计策略(test-case-design strategy)中讨论。

 

白盒测试

测试的另一个策略称为白盒测试,或者说逻辑驱动测试,要求你查看程序的内部结构。这类测试的测试数据来源于对程序逻辑结构的分析(但不幸的是,经常忽略它的规格(specifications))。

 

现在,这种策略的目标和黑盒测试一样,完全测试。使程序的每一条语句至少执行一次,但不难发现仅仅这样还远远不够。此刻不赘述,第四章中有详细说明。作为类比我们考虑所有的可能出现的执行路径。也就是说,如果你的 测试案例(test cases)穷举了所有的可执行路径,那么有可能说你的程序通过了完全测试。

 

这个论断有两点问题。第一:一个程序的逻辑路径可能很多。为了说明,考虑图2.1这个普通程序的个流程图。每一个节点或循环,表示一组可执行语句,可能由分支语句终止。每一条边或弧线表示语句中的控制转移。这个图描述了一个1020条语句组成的程序,这个程序含有DO循环,它迭代了20次。在DO循环中有一个IF的嵌套语句。确定这个程序的所有的逻辑路径也就是确定由点a移到点b的所有路径(假定程序中所有判断都是不相关的)。这个数据大概是1014 ,或者说100 亿,由520+519+…51计算而得。许多人不能清楚的认识这个数据,考虑以下情况:如果一个人写,执行,验证一个测试案例需要5分钟,那他大概要10亿年才能执行完所有的测试案例。如果你的效率提高300倍,也就是一秒钟完成一个,那大概需要320万年。

 

 

当然,实际的程序中不是每条判断语句都是独立的,意味着某些可执行路径要少一些。但是,实际的程序比图2-1中的程序要大的多。因此,穷举所有的可执行路径,像穷举所有输入一样,即使可能,也是不实际的。

 

“穷举所有可执行路径就是完全测试”包含的第二个错误是即使每一条路径均测试了,程序仍可能含有错误。有以下三点解释。

 

第一,穷举所有路径不能保证程序符合规格。例如,如果你被要求写一个升序的程序但写成了降序的,穷举测试不会有任何价值;因为程序本身是错误的,没有符合要求。

 

第二,一个程序自身可能漏掉了某些路径。穷举所有路径的测试,当然,检测不到这些漏掉的路径。

 

第三,即便穷举了所有的路径,程序仍可能还有对数据敏感的错误。有许多这种类型的错误,举一个简单例子。假定一个程序比较两个数据是否相差不大,也就是说,看这两个数据之差是否小于某一个确定的数。如,你可能写了如下一个JavaIF语句:

                        If(a-b<c)

                           System.out.println(“a-b<c”);

 

程序包含了一个明显错误,因为它没把a-b的绝对值和c比较。能检测到此类错误,依赖于ab的值的选择。仅仅穷举所有的可执行路径并不能一定能找到此类错误。

 

总结下,虽然穷举输入测试比穷举所有路径的测试更可靠些,由于不可行,没有一种是有用的。把黑盒和白盒测试结合起来有可能产生一些有价值的测试策略。这将在第四章中讨论。

 

软件测试原理

继续本章论题,软件测试中最重要的考虑是心理学,从中我们可以辨认出一些重要的原理或指南。许多原理都是显然的,但经常被忽略。表2.1列出了这些重要原理,每一条在下面都有详细的叙述。

 

2.1

重要的软件测试指南

原理1

预期结果是测试案例的一个必要组成部分

原理2

程序员应当尽力避免测试其自己的程序

原理3

某个组织不应当测试其自己的程序

原理4

仔细检查每一次测试的结果

原理5

测试案例不仅要包含有效的输入,还要包含无效的输入

原理6

检查程序是否做了该做的事,只对了一半;另一半是检验程序是否做了它不该做的事

原理7

不要扔掉测试案例除非程序本身就毫无价值

原理8

设计测试案例时不要默认假定不会找到错误

原理9

程序的某一部分可能存在的错误正比于已经发现的错误

原理10

测试需要极高的创造性和丰富的知识

 

原理1:预期结果是测试案例的一个必要组成部分。

无预期结果是软件测试中出现频率最高的一种错误。这又和人的心理有关。当预期值没有在测试案例中写出时,很可能把一些似乎合理的,但是错误的结果当做正确的,因为“眼睛看到它希望看到的结果。”换种说话,虽然把测试定义为破坏性过程,但是潜意识里我们希望看到正确的结果。解决这个问题的方法之一是,预先把程序的输出详细写出来。因此,测试案例必需包含以下两部分:

 

1.  程序输入数据的描述

2.  程序相对于输入数据的预期值的详细描述。

            

当看到不寻常的,或者说不符和我们预期值的输出时,这应当被认为是正常的。如果某些问题看起来可能有问题,那么有某种预期结果就显得相当必要的。因为没有期望,就没有意外嘛。

 

原理2:程序员应当避免测试其自己的程序。

每一个作家都知道-或者说应当知道-编辑或校正自己的作品不是个好主意。你认为某段话表示某个含义但没有意识到可能有别的理解。并且不希望找到自己文章中的错误。这和软件作者的心理是一致的。

 

另一个问题是由对软件注意点的改变引起的。当一个程序员以建设性的观点来写程序时(创建程序是建设性过程),很难让他立刻以破坏性的观点来看待软件。

 

许多房主知道,把壁纸取走(破坏性过程)很难,特别当那些壁纸是你亲自放上去的时候。相似的,许多程序员不能有效的测试他们自己的程序,因为他们心理上就不愿看到自己的程序有错误。另外,程序员由于怕来自上司,客户,或者程序的所有者方面带来的损失,潜意识里会阻止自己发现程序的错误。

 

除了这些心理问题外,第二个问题是:程序仍可能含有错误,这类错误来源于对规格的错误理解。如果是此类问题,程序员很可能在测试时仍持相同的错误观点。

 

这并不意味着程序员不能够测试他自己的程序。而是说,如果由另一个人来测试,会更有效。

 

注意这些讨论不能应用于调试(debugging)(改正已经知道的错误);调试由程序的作者来做将更有效。

原理3:某个组织不应当测试自己的程序。

这条原理和上一条类似。一个项目或组织,在许多方面有共通点。从更深入的角度讲,一个组织或项目经理的绩效主要由其在一定开支内完成的程序数量来衡量。由于时间和开支很容易计算,但软件的质量和可靠性却难以衡量。因此一个组织很难客观的测试他们自己的程序。因为假设我们对测试的定义是恰当的,那么测试将可能降低他们的绩效。

 

再一次强调,这并不表示组织内不能找到自己的错误,因为他们毕竟有相当多的成功经验。只是说,由一个客观的独立的组织来测试将更合算。

 

原理4:仔细检查每一次测试的结果

这可能是最明显的原理,但同样常常被忽视。许多情况下不能找到某些错误,尽管这些错误可能很明显。比如说,某个错误,在晚一些的版本中发现了,但它早就存在。

原理5:测试案例不仅要包含有效的输入,还要包含无效的输入。

自然的,容易把注意力集中在程序的有效输入上,而忽略了无效的输入。这种测试倾向经常出现在对第一章三角形程序的测试之中。

 

很少人把125作为输入条件,检验程序是否把它判断为无效的三角形而非不等边三角形。如果输入一些无效的,或非预期的值很可能立刻就找到错误。因此,如果测试案例中包含无效输入,那将比有效的输入更可能找到错误。

原理6:检查程序是否做了该做的事,只对了一半;另一半是检验程序是否做了它不该做的事。

这是上一条原理的必然推论。程序必须检验不需要的副作用。例如,一个薪水程序产生了正确的薪水,但它很可能还有错误,例如多产生了一个不存在的雇员的薪水或者把某个私有文件的第一条记录改写了。

原理7:不要扔掉测试案例除非程序本身就毫无价值

这种问题在交互性系统中经常发生。此类测试的一个常见方法是,坐在终端前凭空设想测试案例,然后开始测试程序。这引起的问题是,测试案例是有价值的,但当测试结束时,他们也被扔掉了。如果程序被再一次测试(例如,修正了某个错误或者增加了功能),必须重新设计测试案例。许多情况下,由于重新设计测试案例需要可观的工作量,因此很容易被忽略。所以,再一次测试时,很可能没有上一次严格,这就意味着当修改引起了之前某一个功能的失败,这类错误很可能查不出来。保留测试案例当以后某些部分更改了能重新测试,叫做-回归测试。

原理8:设计测试案例时不要默认假定不会找到错误。

这是项目经理经常发生的错误,这归因于对测试的错误定义-测试是表明程序工作正常。再一次强调,测试的定义是找错的过程。

原理9:程序的某一部分可能存在的错误正比于已经发现的错误

2.2表明了这种现象。初次看这个图,可能认为它没有什么意义,但这种现象在大量的程序中出现。例如,一个程序有两个模块,或者说两个类,两个子程序AB,如果在A中发现了5个错误,而B中发现了1个错误,如果没有有意识的对A 进行严格测试,那么这条原理告诉我们A中仍存在的错误比B多。

这条原理的另一种理解是,错误倾向于成堆出现,在典型的程序中,某些部分更可能出错,虽然还没人提出好的解释。这个现象给予我们某些启示:如果程序的某一部分更可能出错,那么我们因该更加关注这一部分。

原理10:测试需要极高的创造性和丰富的知识

很可能测试需要的创造性超过了设计软件需要的创造性。我们已经知道不论怎样测试也不能找出程序的所有错误。本书后面章节将讨论怎样设计一些好的测试案例,但怎样有效的利用这些方法仍需要大量的创造性。

 

总结:

当你继续看本书时,记住测试的3条重要原理:
测试是挑错过程

     好的测试案例更可能找到错误

     成功的测试案例能找到错误。

 

 
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值