软件测试是为了发现错误而执行程序的过程。或者说,软件测试是根据软件开发各阶段的规格说明和程序的内部结构而精心设计一批测试用例(即输入数据及其预期的输出结果),并利用这些测试用例去运行程序,以发现程序错误的过程。
软件测试在软件生存期中横跨两个阶段:通常在编写出每一个模块之后就对它做必要的测试(称为单元测试)。模块的编写者与测试者是同一个人。编码与单元测试属于软件生存期中的同一个阶段。在这个阶段结束之后,对软件系统还要进行各种综合测试,这是软件生存期的另一个独立的阶段,即测试阶段,通常由专门的测试人员承担这项工作。
设计测试的目标是想以最少的时间和人力系统地找出软件中潜在的各种错误和缺陷。
软件测试的目的,第一是确认软件的质量,其一方面是确认软件做了你所期望的事情(Do the right thing),另一方面是确认软件以正确的方式来做了这个事件(Do it right)。第二是提供信息,比如提供给开发人员或程序经理的回馈信息,为风险评估所准备的信息。第三软件测试不仅是在测试软件产品的本身,而且还包括软件开发的过程。如果一个软件产品开发完成之后发现了很多问题,这说明此软件开发过程很可能是有缺陷的。因此软件测试的第三个目的是保证整个软件开发过程是高质量的。
测试人员在软件开发过程中的任务:
1、寻找Bug;
2、避免软件开发过程中的缺陷;
3、衡量软件的质量;
4、关注用户的需求。
软件测试的原则:
① 应当把“尽早地和不断地进行软件测试”作为软件开发者的座右铭。
不应把软件测试仅仅看作是软件开发的一个独立阶段,而应当把它贯穿到软件开发的各个阶段中。坚持在软件开发的各个阶段的技术评审,这样才能在开发过程中尽早发现和预防错误,把出现的错误克服在早期,杜绝某些发生错误的隐患。
② 测试用例应由测试输入数据和与之对应的预期输出结果这两部分组成。
测试以前应当根据测试的要求选择测试用例(Test case),用来检验程序员编制的程序,因此不但需要测试的输入数据,而且需要针对这些输入数据的预期输出结果。
③ 程序员应避免检查自己的程序。
程序员应尽可能避免测试自己编写的程序,程序开发小组也应尽可能避免测试本小组开发的程序。如果条件允许,最好建立独立的软件测试小组或测试机构。这点不能与程序的调试(debuging)相混淆。调试由程序员自己来做可能更有效。
④ 在设计测试用例时,应当包括合理的输入条件和不合理的输入条件。
合理的输入条件是指能验证程序正确的输入条件,不合理的输入条件是指异常的,临界的,可能引起问题异变的输入条件。软件系统处理非法命令的能力必须在测试时受到检验。用不合理的输入条件测试程序时,往往比用合理的输入条件进行测试能发现更多的错误。
⑤ 充分注意测试中的群集现象。
在被测程序段中,若发现错误数目多,则残存错误数目也比较多。这种错误群集性现象,已为许多程序的测试实践所证实。根据这个规律,应当对错误群集的程序段进行重点测试,以提高测试投资的效益。
⑥ 严格执行测试计划,排除测试的随意性。
测试之前应仔细考虑测试的项目,对每一项测试做出周密的计划,包括被测程序的功能、输入和输出、测试内容、进度安排、资源要求、测试用例的选择、测试的控制方式和过程等,还要包括系统的组装方式、跟踪规程、调试规程,回归测试的规定,以及评价标准等。对于测试计划,要明确规定,不要随意解释。
⑦ 应当对每一个测试结果做全面检查。
有些错误的征兆在输出实测结果时已经明显地出现了,但是如果不仔细地全面地检查测试结果,就会使这些错误被遗漏掉。所以必须对预期的输出结果明确定义,对实测的结果仔细分析检查,抓住征侯,暴露错误。
⑧ 妥善保存测试计划,测试用例,出错统计和最终分析报告,为维护提供方便。
确认(Validation)是一系列的活动和过程,其目的是想证实在一个给定的外部环境中软件的逻辑正确性。它包括需求规格说明的确认和程序的确认,而程序的确认又分为静态确认与动态确认。静态确认一般不在计算机上实际执行程序,而是通过人工分析或者程序正确性证明来确认程序的正确性; 动态确认主要通过动态分析和程序测试来检查程序的执行状态,以确认程序是否有问题。
验证(Verification),则试图证明在软件生存期各个阶段,以及阶段间的逻辑协调性、完备性和正确性。
确认与验证工作都属于软件测试。在对需求理解与表达的正确性、设计与表达的正确性、实现的正确性以及运行的正确性的验证中,任何一个环节上发生了问题都可能在软件测试中表现出来。
测试信息流如图1所示。测试过程需要三类输入:
§ 软件配置:包括软件需求规格说明、软件设计规格说明、源代码等;
§ 测试配置:包括测试计划、测试用例、测试驱动程序等;
§ 测试工具:测试工具为测试的实施提供某种服务。例如,测试数据自动生成程序、静态分析程序、动态分析程序、测试结果分析程序、以及驱动测试的工作台等。
测试之后,用实测结果与预期结果进行比较。如果发现出错的数据,就要进行调试。对已经发现的错误进行错误定位和确定出错性质,并改正这些错误,同时修改相关的文档。修正后的文档一般都要经过再次测试,直到通过测试为止。
通过收集和分析测试结果数据,对软件建立可靠性模型。
图1 测试信息流
如果测试发现不了错误,那么可以肯定,测试配置考虑得不够细致充分,错误仍然潜伏在软件中。这些错误最终不得不由用户在使用中发现,并在维护时由开发者去改正。但那时改正错误的费用将比在开发阶段改正错误的费用要高出40倍到60倍。
软件开发过程是一个自顶向下,逐步细化的过程,而测试过程则是依相反的顺序安排的 自底向上,逐步集成的过程。低一级测试为上一级测试准备条件。参看图1.2,首先对每一个程序模块进行单元测试,消除程序模块内部在逻辑上和功能上的错误和缺陷。再对照软件设计进行集成测试,检测和排除子系统(或系统)结构上的错误。随后再对照需求,进行确认测试。最后从系统全体出发,运行系统,看是否满足要求。
图2 软件测试与软件开发过程的关系
由于人们对错误有不同的理解和认识,所以目前还没有一个统一的错误分类方法。错误难于分类的原因,一方面是由于一个错误有许多征兆,因而它可以被归入不同的类。另一方面是因为把一个给定的错误归于哪一类,还与错误的来源和程序员的心理状态有关。
§ 较小错误:只对系统输出有一些非实质性影响。如,输出的数据格式不合要求等。
§ 中等错误:对系统的运行有局部影响。如输出的某些数据有错误或出现冗余。
§ 较严重错误:系统的行为因错误的干扰而出现明显不合情理的现象。比如开出了0.00元的支票,系统的输出完全不可信赖。
§ 严重错误:系统运行不可跟踪,一时不能掌握其规律,时好时坏。
§ 非常严重的错误:系统运行中突然停机,其原因不明,无法软启动。
§ 最严重的错误:系统运行导致环境破坏,或是造成事故,引起生命、财产的损失。
① 功能错误
§ 规格说明错误:规格说明可能不完全,有二义性或自身矛盾。
§ 功能错误:程序实现的功能与用户要求的不一致。这常常是由于规格说明中包含错误的功能、多余的功能或遗漏的功能所致。
§ 测试错误:软件测试的设计与实施发生错误。软件测试自身也可能发生错误。
§ 测试标准引起的错误:对软件测试的标准要选择适当,若测试标准太复杂,则导致测试过程出错的可能就大。
② 系统错误
§ 外部接口错误:外部接口指如终端、打印机、通信线路等系统与外部环境通信的手段。所有外部接口之间,人与机器之间的通信都使用形式的或非形式的专门协议。如果协议有错,或太复杂,难以理解,致使在使用中出错。此外还包括对输入/输出格式错误理解,对输入数据不合理的容错等等。
§ 内部接口错误:内部接口指程序之间的联系。它所发生的错误与程序内实现的细节有关。例如,设计协议错、输入/输出格式错、数据保护不可靠、子程序访问错等。
§ 硬件结构错误:这类错误在于不能正确地理解硬件如何工作。例如,忽视或错误地理解分页机构、位址生成、信道容量、I/O指令、中断处理、设备初始化和启动等而导致的出错。
§ 操作系统错误:这类错误主要是由于不了解操作系统的工作机制而导致出错。当然,操作系统本身也有错误,但是一般用户很难发现这种错误。
§ 软件结构错误:由于软件结构不合理或不清晰而引起的错误。这种错误通常与系统的负载有关,而且往往在系统满载时才出现。这是最难发现的一类错误。例如,错误地设置局部参数或全局参数;错误地假定寄存器与内存单元初始化了;错误地假定不会发生中断而导致不能封锁或开中断;错误地假定程序可以绕过数据的内部锁而导致不能关闭或打开内部锁;错误地假定被调用子程序常驻内存或非常驻内存等等,都将导致软件出错。
§ 控制与顺序错误:这类错误包括:忽视了时间因素而破坏了事件的顺序;猜测事件出现在指定的序列中;等待一个不可能发生的条件;漏掉先决条件;规定错误的优先级或程序状态;漏掉处理步骤;存在不正确的处理步骤或多余的处理步骤等。
§ 资源管理错误:这类错误是由于不正确地使用资源而产生的。例如,使用未经获准的资源;使用后未释放资源;资源死锁;把资源链接在错误的序列中等等。
③ 加工错误
§ 算术与操作错误:指在算术运算、函数求值和一般操作过程中发生的错误。包括:数据类型转换错;除法溢出;错误地使用关系比较符;用整数与浮点数做比较等。
§ 初始化错误:典型的错误有:忘记初始化工作区,忘记初始化寄存器和资料区;错误地对循环控制变量赋初值;用不正确的格式,数据或类型进行初始化等等。
§ 控制和次序错误:这类错误与系统级同名错误类似,但它是局部错误。包括:遗漏路径;不可达到的代码;不符合语法的循环嵌套;循环返回和终止的条件不正确;漏掉处理步骤或处理步骤有错等。
§ 静态逻辑错误:这类错误主要包括:不正确地使用CASE语句;在表达式中使用不正确的否定(例如用“>”代替“<”的否定);对情况不适当地分解与组合;混淆“或”与“异或”等。
④ 数据错误
§ 动态数据错误:动态数据是在程序执行过程中暂时存在的数据。各种不同类型的动态数据在程序执行期间将共享一个共同的存储区域,若程序启动时对这个区域未初始化,就会导致数据出错。由于动态数据被破坏的位置可能与出错的位置在距离上相差很远,因此要发现这类错误比较困难。
§ 静态数据错误:静态数据在内容和格式上都是固定的。它们直接或间接地出现在程序或数据库中。由编译程序或其它专门程序对它们做预处理。这是在程序执行前防止静态错误的好办法,但预处理也会出错。
§ 数据内容错误:数据内容是指存储于存储单元或数据结构中的位串、字符串或数字。数据内容本身没有特定的含义,除非通过硬件或软件给予解释。数据内容错误就是由于内容被破坏或被错误地解释而造成的错误。
§ 数据结构错误:数据结构是指数据元素的大小和组织形式。在同一存储区域中可以定义不同的数据结构。数据结构错误主要包括结构说明错误及把一个数据结构误当做另一类数据结构使用的错误。这是更危险的错误。
§ 数据属性错误:数据属性是指数据内容的含义或语义。例如,整数、字符串、子程序等等。数据属性错误主要包括:对数据属性不正确地解释,比如错把整数当实数,允许不同类型数据混合运算而导致的错误等。
⑤ 代码错误
主要包括:语法错误;打字错误;对语句或指令不正确理解所产生的错误。
Good enough-Gerhart分类方法把软件的逻辑错误按生存期不同阶段分为4类。
① 问题定义(需求分析)错误
它们是在软件定义阶段,分析员研究用户的要求后所编写的文档中出现的错误。换句话说,这类错误是由于问题定义不满足用户的要求而导致的错误。
② 规格说明错误
这类错误是指规格说明与问题定义不一致所产生的错误。它们又可以细分成:
§ 不一致性错误:规格说明中功能说明与问题定义发生矛盾。
§ 冗余性错误:规格说明中某些功能说明与问题定义相比是多余的。
§ 不完整性错误:规格说明中缺少某些必要的功能说明。
§ 不可行错误:规格说明中有些功能要求是不可行的。
§ 不可测试错误:有些功能的测试要求是不现实的。
3设计错误
这是在设计阶段产生的错误,它使系统的设计与需求规格说明中的功能说明不相符。它们又可以细分为:
§ 设计不完全错误:某些功能没有被设计,或设计得不完全。
§ 算法错误:算法选择不合适。主要表现为算法的基本功能不满足功能要求、算法不可行或者算法的效率不符合要求。
§ 模块接口错误:模块结构不合理;模块与外部数据库的接口不一致,模块之间的接口不一致。
§ 控制逻辑错误:控制流程与规格说明不一致;控制结构不合理。
§ 数据结构错误:数据设计不合理;与算法不匹配;数据结构不满足规格说明要求。
④ 编码错误
编码过程中的错误是多种多样的,大体可归为以下几种:数据说明错、数据使用错、计算错、比较错、控制流错、接口错、输入/输出错,及其它的错误。
在不同的开发阶段,错误的类型和表现形式是不同的,故应当采用不同的方法和策略来进行检测。
测试过程按4个步骤进行,即单元测试、组装测试、确认测试和系统测试。图2.1显示出软件测试经历的4个步骤。单元测试集中对用源代码实现的每一个程序单元进行测试,检查各个程序模块是否正确地实现了规定的功能。然后,进行集成测试,根据设计规定的软件体系结构,把已测试过的模块组装起来,在组装过程中,检查程序结构组装的正确性。确认测试则是要检查已实现的软件是否满足了需求规格说明中确定了的各种需求,以及软件配置是否完全、正确。最后是系统测试,把已经经过确认的软件纳入实际运行环境中,与其它系统成份组合在一起进行测试。严格地说,系统测试已超出了软件工程的范围。
图3 软件测试的过程
单元测试的对象是软件设计的最小单位——模块。单元测试的依据是详细设计描述,单元测试应对模块内所有重要的控制路径设计测试用例,以便发现模块内部的错误。单元测试多采用白盒测试技术,系统内多个模块可以并行地进行测试。
单元测试任务:
单元测试任务包括:1 模块接口测试;2 模块局部数据结构测试;3 模块边界条件测试;4 模块中所有独立执行通路测试;5 模块的各条错误处理通路测试。
模块接口测试是单元测试的基础。只有在数据能正确流入、流出模块的前提下,其它测试才有意义。测试接口正确与否应该考虑下列因素:
§ 输入的实际参数与形式参数的个数是否相同;
§ 输入的实际参数与形式参数的属性是否匹配;
§ 输入的实际参数与形式参数的量纲是否一致;
§ 调用其它模块时所给实际参数的个数是否与被调模块的形参个数相同;
§ 调用其它模块时所给实际参数的属性是否与被调模块的形参属性匹配;
§调用其它模块时所给实际参数的量纲是否与被调模块的形参量纲一致;
§ 调用预定义函数时所用参数的个数、属性和次序是否正确;
§ 是否存在与当前入口点无关的参数引用;
§ 是否修改了只读型参数;
§ 对全程变量的定义各模块是否一致;
§ 是否把某些约束作为参数传递。
如果模块内包括外部输入输出,还应该考虑下列因素:
§ 文件属性是否正确;
§ OPEN/CLOSE语句是否正确;
§ 格式说明与输入输出语句是否匹配;
§ 缓冲区大小与记录长度是否匹配;
§ 檔使用前是否已经打开;
§ 是否处理了档尾;
§ 是否处理了输入/输出错误;
§ 输出信息中是否有文字性错误;
检查局部数据结构是为了保证临时存储在模块内的数据在程序执行过程中完整、正确。局部数据结构往往是错误的根源,应仔细设计测试用例,力求发现下面几类错误:
§ 不合适或不相容的类型说明;
§ 变数无初值;
§ 变数初始化或省缺值有错;
§ 不正确的变量名(拼错或不正确地截断);
§ 出现上溢、下溢和地址异常。
除了局部数据结构外,如果可能,单元测试时还应该查清全局数据(例如FORTRAN的公用区)对模块的影响。
在模块中应对每一条独立执行路径进行测试,单元测试的基本任务是保证模块中每条语句至少执行一次。此时设计测试用例是为了发现因错误计算、不正确的比较和不适当的控制流造成的错误。此时基本路径测试和循环测试是最常用且最有效的测试技术。计算中常见的错误包括:
§ 误解或用错了算符优先级;
§ 混合类型运算;
§ 变数初值错;
§ 精度不够;
§ 表达式符号错。
比较判断与控制流常常紧密相关,测试用例还应致力于发现下列错误:
§ 不同数据类型的对象之间进行比较;
§ 错误地使用逻辑运算符或优先级;
§ 因计算机表示的局限性,期望理论上相等而实际上不相等的两个量相等;
§ 比较运算或变量出错;
§ 循环终止条件或不可能出现;
§ 迭代发散时不能退出;
§ 错误地修改了循环变量。
一个好的设计应能预见各种出错条件,并预设各种出错处理通路,出错处理通路同样需要认真测试,测试应着重检查下列问题:
§ 输出的出错信息难以理解;
§ 记录的错误与实际遇到的错误不相符;
§ 在程序自定义的出错处理段运行之前,系统已介入;
§ 异常处理不当;
§ 错误陈述中未能提供足够的定位出错信息。
边界条件测试是单元测试中最后,也是最重要的一项任务。众的周知,软件经常在边界上失效,采用边界值分析技术,针对边界值及其左、右设计测试用例,很有可能发现新的错误。
单元测试过程
一般认为单元测试应紧接在编码之后,当源程序编制完成并通过复审和编译检查,便可开始单元测试。测试用例的设计应与复审工作相结合,根据设计信息选取测试数据,将增大发现上述各类错误的可能性。在确定测试用例的同时,应给出期望结果。
应为测试模块开发一个驱动模块(driver)和(或)若干个桩模块(stub),下图显示了一般单元测试的环境。驱动模块在大多数场合称为“主程序”,它接收测试数据并将这些数据传递到被测试模块,被测试模块被调用后,“主程序”打印“进入-退出”消息。
驱动模块和桩模块是测试使用的软件,而不是软件产品的组成部分,但它需要一定的开发费用。若驱动和桩模块比较简单,实际开销相对低些。遗憾的是,仅用简单的驱动模块和桩模块不能完成某些模块的测试任务,这些模块的单元测试只能采用下面讨论的综合测试方法。
提高模块的内聚度可简化单元测试,如果每个模块只能完成一个,所需测试用例数目将显著减少,模块中的错误也更容易发现。
在单元测试的基础上,需要将所有模块按照设计要求组装成为系统。这时需要考虑:
§ 在把各个模块连接起来的时侯,穿越模块接口的数据是否会丢失;
§ 一个模块的功能是否会对另一个模块的功能产生不利的影响;
§ 各个子功能组合起来,能否达到预期要求的父功能;
§ 全局数据结构是否有问题;
§ 单个模块的误差累积起来,是否会放大,从而达到不能接受的程度。
§ 单个模块的错误是否会导致数据库错误。
选择什么方式把模块组装起来形成一个可运行的系统,直接影响到模块测试用例的形式、所用测试工具的类型、模块编号的次序和测试的次序、以及生成测试用例的费用和调试的费用。通常,把模块组装成为系统的方式有两种方式:
① 一次性集成方式
它是一种非增殖式集成方式。也叫做整体拼装。使用这种方式,首先对每个模块分别进行模块测试,然后再把所有模块组装在一起进行测试,最终得到要求的软件系统。
由于程序中不可避免地存在涉及模块间接口、全局数据结构等方面的问题,所以一次试运行成功的可能性并不很大。
② 增殖式集成方式
又称渐增式集成方式。首先对一个个模块进行模块测试,然后将这些模块逐步组装成较大的系统,在组装的过程中边连接边测试,以发现连接过程中产生的问题。最后通过增殖逐步组装成为要求的软件系统。
§ 自顶向下的增殖方式:将模块按系统程序结构,沿控制层次自顶向下进行集成。由于这种增殖方式在测试过程中较早地验证了主要的控制和判断点。在一个功能划分合理的程序结构中,判断常出现在较高的层次,较早就能遇到。如果主要控制有问题,尽早发现它能够减少以后的返工。
§ 自底向上的增殖方式:从程序结构的最底层模块开始组装和测试。因为模块是自底向上进行组装,对于一个给定层次的模块,它的子模块(包括子模块的所有下属模块)已经组装并测试完成,所以不再需要桩模块。在模块的测试过程中需要从子模块得到的信息可以直接运行子模块得到。
③ 混合增殖式测试:自顶向下增殖的方式和自底向上增殖的方式各有优缺点。自顶向下增殖方式的缺点是需要建立桩模块。要使桩模块能够模拟实际子模块的功能将是十分困难的。同时涉及复杂算法和真正输入/输出的模块一般在底层,它们是最容易出问题的模块,到组装和测试的后期才遇到这些模块,一旦发现问题,导致过多的回归测试。而自顶向下增殖方式的优点是能够较早地发现在主要控制方面的问题。自底向上增殖方式的缺点是“程序一直未能做为一个实体存在,直到最后一个模块加上去后才形成一个实体”。就是说,在自底向上组装和测试的过程中,对主要的控制直到最后才接触到。但这种方式的优点是不需要桩模块,而建立驱动模块一般比建立桩模块容易,同时由于涉及到复杂算法和真正输入/输出的模块最先得到组装和测试,可以把最容易出问题的部分在早期解决。此外自底向上增殖的方式可以实施多个模块的并行测试。
有鉴于此,通常是把以上两种方式结合起来进行组装和测试。
§ 衍变的自顶向下的增殖测试:它的基本思想是强化对输入/输出模块和引入新算法模块的测试,并自底向上组装成为功能相当完整且相对独立的子系统,然后由主模块开始自顶向下进行增殖测试。
§ 自底向上-自顶向下的增殖测试:它首先对含读操作的子系统自底向上直至根结点模块进行组装和测试,然后对含写操作的子系统做自顶向下的组装与测试。
§ 回归测试:这种方式采取自顶向下的方式测试被修改的模块及其子模块,然后将这一部分视为子系统,再自底向上测试,以检查该子系统与其上级模块的接口是否适配。
确认测试又称有效性测试。它的任务是验证软件的有效性,即验证软件的功能和性能及其它特性是否与用户的要求一致。在软件需求规格说明书描述了全部用户可见的软件属性,其中有一节叫做有效性准则,它包含的信息就是软件确认测试的基础。
在确认测试阶段需要做的工作如图5.5所示。首先要进行有效性测试以及软件配置复审,然后进行验收测试和安装测试,在通过了专家鉴定之后,才能成为可交付的软件。
图4 确认测试的步骤
① 进行有效性测试(功能测试)
有效性测试是在模拟的环境(可能就是开发的环境)下,运用黑盒测试的方法,验证被测软件是否满足需求规格说明书列出的需求。为此,需要首先制定测试计划,规定要做测试的种类。还需要制定一组测试步骤,描述具体的测试用例。通过实施预定的测试计划和测试步骤,确定软件的特性是否与需求相符,确保所有的软件功能需求都能得到满足,所有的软件性能需求都能达到,所有的文档都是正确且便于使用。同时,对其他软件需求,例如可移植性、兼容性、出错自动恢复、可维护性等,也都要进行测试,确认是否满足。
② 软件配置复查
软件配置复查的目的是保证软件配置的所有成分都齐全,各方面的质量都符合要求,具有维护阶段所必需的细节,而且已经编排好分类的目录。
除了按合同规定的内容和要求,由人工审查软件配置之外,在确认测试的过程中,应当严格遵守用户手册和操作手册中规定的使用步骤,以便检查这些文文件数据的完整性和正确性。必须仔细记录发现的遗漏和错误,并且适当地补充和改正。
③ 验收测试
在通过了系统的有效性测试及软件配置审查之后,就应开始系统的验收测试。验收测试是以用户为主的测试。软件开发人员和QA(质量保证)人员也应参加。由用户参加设计测试用例,使用用户接口输入测试数据,并分析测试的输出结果。一般使用生产中的实际数据进行测试。在测试过程中,除了考虑软件的功能和性能外,还应对软件的可移植性、兼容性、可维护性、错误的恢复功能等进行确认。
④ α测试和β测试
在软件交付使用之后,用户将如何实际使用程序,对于开发者来说是无法预测的。因为用户在使用过程中常常会发生对使用方法的误解、异常的数据组合、以及产生对某些用户来说似乎是清晰的但对另一些用户来说却难以理解的输出等等。
如果软件是为多个用户开发的产品的时侯,让每个用户逐个执行正式的验收测试是不切实际的。很多软件产品生产者采用一种称之为α测试和β测试的测试方法,以发现可能只有最终用户才能发现的错误。
α测试是由一个用户在开发环境下进行的测试,也可以是公司内部的用户在仿真实际操作环境下进行的测试。这是在受控制的环境下进行的测试。α测试的目的是评价软件产品的FURPS(即功能、可使用性、可靠性、性能和支持)。尤其注重产品的接口和特色。α测试人员是除开产品开发人员之外首先见到产品的人,他们提出的功能和修改意见是特别有价值的。α测试可以从软件产品编码结束之时开始,或在模块(子系统)测试完成之后开始,也可以在确认测试过程中产品达到一定的稳定和可靠程度之后再开始。有关的手册(草稿)等应事先准备好。
β测试是由软件的多个用户在一个或多个用户的实际使用环境下进行的测试。与α测试不同的是,开发者通常不在测试现场。因而,β测试是在开发者无法控制的环境下进行的软件现场应用。在β测试中,由用户记下遇到的所有问题,包括真实的以及主观认定的,定期向开发者报告,开发者在综合用户的报告之后,做出修改,最后将软件产品交付给全体用户使用。β测试主要衡量产品的FURPS。着重于产品的支持性,包括文档、客户培训和支持产品生产能力。只有当α测试达到一定的可靠程度时,才能开始β测试。由于它处在整个测试的最后阶段,不能指望这时发现主要问题。同时,产品的所有手册文本也应该在此阶段完全定稿。由于β测试的主要目标是测试可支持性,所以β测试应尽可能由主持产品发行的人员来管理。
所谓系统测试,是将通过确认测试的软件,作为整个基于计算机系统的一个元素,与计算机硬件、外设、某些支持软件、数据和人员等其它系统元素结合在一起,在实际运行(使用)环境下,对计算机系统进行一系列的组装测试和确认测试。
系统测试的目的在于通过与系统的需求定义作比较,发现软件与系统定义不符合或与之矛盾的地方。系统测试的测试用例应根据需求分析规格说明来设计,并在实际使用环境下来运行。
① 黑盒测试
根据软件产品的功能设计规格,在计算机上进行测试,以证实每个实现了的功能是否符合要求。这种测试方法就是黑盒测试。黑盒测试意味着测试要在软件的接口处进行。就是说,这种方法是把测试对象看做一个黑盒子,测试人员完全不考虑程序内部的逻辑结构和内部特性,只依据程序的需求分析规格说明,检查程序的功能是否符合它的功能说明。
用黑盒测试发现程序中的错误,必须在所有可能的输入条件和输出条件中确定测试数据,来检查程序是否都能产生正确的输出。
② 白盒测试
根据软件产品的内部工作过程,在计算机上进行测试,以证实每种内部操作是否符合设计规格要求,所有内部成分是否已经过检查。这种测试方法就是白盒测试。白盒测试把测试对象看做一个打开的盒子,允许测试人员利用程序内部的逻辑结构及有关信息,设计或选择测试用例,对程序所有逻辑路径进行测试。通过在不同点检查程序的状态,确定实际的状态是否与预期的状态一致。
不论黑盒测试,还是白盒测试,都不可能进行所谓的穷举测试。任何软件开发项目都要受到期限、费用、人力和机时等条件的限制,为了节省时间和资源,提高测试效率,就必须要从数量极大的可用测试用例中精心地挑选少量的测试数据,使得采用这些测试数据能够达到最佳的测试效果,能够高效率地把隐藏的错误揭露出来。
3 基于风险的测试
基于风险的测试是指评估测试的优先级,先做高优先级的测试,如果时间或精力不够,低优先级的测试可以暂时先不做。如图5 ,横轴代表影响,竖轴代表概率,根据一个软件的特点来确定:如果一个功能出了问题,它对整个产品的影响有多大,这个功能出问题的概率有多大?如果出问题的概率很大,出了问题对整个产品的影响也很大,那么在测试时就一定要覆盖到。对于一个用户很少用到的功能,出问题的概率很小,就算出了问题的影响也不是很大,那么如果时间比较紧的话,就可以考虑不测试。
图5
基于风险测试的两个决定因素就是:该功能出问题对用户的影响有多大,出问题的概率有多大。其它一些影响因素还有复杂性、可用性、依赖性、可修改性等。测试人员主要根据事情的轻重缓急来决定测试工作的重点。
4基于模型的测试
模型实际上就是用语言把一个系统的行为描述出来,定义出它可能的各种状态,以及它们之间的转换关系,即状态转换图。模型是系统的抽象。基于模型的测试是利用模型来生成相应的测试用例,然后根据实际结果和原先预想的结果的差异来测试系统,过程如图6所示。
图6
逻辑覆盖是以程序内部逻辑结构为基础来设计测试用例的技术,属白盒测试。这一方法要求测试人员对程序的逻辑结构有清楚的了解,甚至要能掌握源程序的所有细节。由于覆盖测试的目标不同,逻辑覆盖又可分为:语句覆盖、判定覆盖、判定-条件覆盖、条件组合覆盖及路径覆盖。
① 语句覆盖 :语句覆盖就是设计若干个测试用例,运行被测程序,使得每一可执行语句至少执行一次。这种覆盖又称为点覆盖,它使得程序中每个可执行语句都得到执行,但它是最弱的逻辑覆盖,效果有限,必须与其它方法交互使用。
② 判定覆盖 :判定覆盖就是设计若干个测试用例,运行被测程序,使得程序中每个判断的取真分支和取假分支至少经历一次。判定覆盖又称为分支覆盖。
判定覆盖只比语句覆盖稍强一些,但实际效果表明,只是判定覆盖,还不能保证一定能查出在判断的条件中存在的错误。因此,还需要更强的逻辑覆盖准则去检验判断内部条件。
③ 条件覆盖 :条件覆盖就是设计若干个测试用例,运行被测程序,使得程序中每个判断的每个条件的可能取值至少执行一次。
条件覆盖深入到判定中的每个条件,但可能不能满足判定覆盖的要求。
④ 判定-条件覆盖 :判定-条件覆盖就是设计足够的测试用例,使得判断中每个条件的所有可能取值至少执行一次,同时每个判断本身的所有可能判断结果至少执行一次。换言之,即是要求各个判断的所有可能的条件取值组合至少执行一次。
判定-条件覆盖有缺陷。从表面上来看,它测试了所有条件的取值。但是事实并非如此。往往某些条件掩盖了另一些条件。会遗漏某些条件取值错误的情况。为彻底地检查所有条件的取值,需要将判定语句中给出的复合条件表达式进行分解,形成由多个基本判定嵌套的流程图。这样就可以有效地检查所有的条件是否正确了。
⑤ 多重条件覆盖 :多重条件覆盖就是设计足够的测试用例,运行被测程序,使得每个判断的所有可能的条件取值组合至少执行一次。
这是一种相当强的覆盖准则,可以有效地检查各种可能的条件取值的组合是否正确。它不但可覆盖所有条件的可能取值的组合,还可覆盖所有判断的可取分支,但可能有的路径会遗漏掉。测试还不完全。
6 路径测试 :路径测试就是设计足够的测试用例,覆盖程序中所有可能的路径。这是最强的覆盖准则。但在路径数目很大时,真正做到完全覆盖是很困难的,必须把覆盖路径数目压缩到一定限度。
(2) 判定覆盖
所谓判定覆盖就是设计若干个测试用例,运行被测程序,使得程序中每个判断的取真分支和取假分支至少经历一次。判定覆盖又称为分支覆盖。
(3) 条件覆盖
所谓条件覆盖就是设计若干个测试用例,运行被测程序,使得程序中每个判断的每个条件的可能取值至少执行一次。例如在本题所给出的例子中,我们事先可对所有条件的取值加以标记。
(4) 判定-条件覆盖
所谓判定-条件覆盖就是设计足够的测试用例,使得判断中每个条件的所有可能取值至少执行一次,同时每个判断本身的所有可能判断结果至少执行一次。换言之,即是要求各个判断的所有可能的条件取值组合至少执行一次
(5) 条件组合覆盖
所谓条件组合覆盖就是设计足够的测试用例,运行被测程序,使得每个判断的所有可能的条件取值组合至少执行一次。
(6) 路径测试
路径测试就是设计足够的测试用例,覆盖程序中所有可能的路径。
等价类划分是一种典型的黑盒测试方法。使用这一方法时,完全不考虑程序的内部结构,只依据程序的规格说明来设计测试用例。由于不可能用所有可以输入的数据来测试程序,而只能从全部可供输入的数据中选择一个子集进行测试。如何选择适当的子集,使其尽可能多地发现错误。解决的办法之一就是等价类划分。
1 把数目极多的输入数据(有效的和无效的)划分为若干等价类。所谓等价类是指某个输入域的子集合,在该子集合中,各个输入数据对于揭露程序中的错误都是等效的。并合理地假定:测试某等价类的代表值就等价于对这一类其它值的测试。因此,我们可以把全部输入资料合理划分为若干等价类,在每一个等价类中取一个数据做为测试的输入条件,就可用少量代表性测试数据,取得较好的测试效果。
等价类的划分有两种不同的情况:
§ 有效等价类:是指对于程序规格说明来说,是合理的,有意义的输入数据构成的集合。利用它,可以检验程序是否实现了规格说明预先规定的功能和性能。
§ 无效等价类:是指对于程序规格说明来说,是不合理的,无意义的输入数据构成的集合。利用它,可以检查程序中功能和性能的实现是否有不符合规格说明要求的地方。
在设计测试用例时,要同时考虑有效等价类和无效等价类的设计。软件不能都只接收合理的数据,还要经受意外的考验,接受无效的或不合理的数据,这样获得的软件才能具有较高的可靠性。划分等价类的原则如下:
§ 按区间划分:如果可能的输入数据属于一个取值范围或值的个数限制范围,则可以确立一个有效等价类和两个无效等价类。
§ 按数值划分:如果规定了输入数据的一组值,而且程序要对每个输入值分别进行处理。则可为每一个输入值确立一个有效等价类,此外针对这组值确立一个无效等价类,它是所有不允许的输入值的集合。
§ 按数值集合划分:如果可能的输入数据属于一个值的集合,或者须满足“必须如何”的条件,这时可确立一个有效等价类和一个无效等价类。
§ 按限制条件或规则划分:如果规定了输入数据必须遵守的规则或限制条件,则可以确立一个有效等价类(符合规则)和若干个无效等价类(从不同角度违反规则)。
② 确立测试用例
在确立了等价类之后,建立等价类表,列出所有划分出的等价类:
再从划分出的等价类中按以下原则选择测试用例:
§ 设计尽可能少的测试用例,覆盖所有的有效等价类;
§ 针对每一个无效等价类,设计一个测试用例来覆盖它。
边界值的分析是利用了一个规律,即程序最容易发生错误的地方就是在边界值的附近,它取决于变量的类型,以及变量的取值范围。比如,在做三角形计算时,要输入三角形的三个边长:A、B和C。 我们应注意到这三个数值应当满足A>0、B>0、C>0、A+B>C、A+C>B、B+C>A,才能构成三角形。但如果把六个不等式中的任何一个大于号“>”错写成大于等于号“≥”,那就不能构成三角形。问题恰出现在容易被疏忽的边界附近。这里所说的边界是指,相当于输入等价类和输出等价类而言,稍高于其边界值及稍低于其边界值的一些特定情况。
使用边界值分析方法设计测试用例,首先应确定边界情况。通常输入等价类与输出等价类的边界,就是应着重测试的边界情况。应当选取正好等于,刚刚大于,或刚刚小于边界的值做为测试数据,而不是选取等价类中的典型值或任意值做为测试资料。一般对于有n个变量时,会有6n+1个测试用例,取值分别是min-1, min, min+1, normal, max-1, max,max+1的组合。
边界值分析方法是最有效的黑盒测试方法,但当边界情况很复杂的时候,要找出适当的测试用例还需针对问题的输入域、输出域边界,耐心细致地逐个考虑。
人们也可以靠经验和直觉推测程序中可能存在的各种错误,从而有针对性地编写检查这些错误的例子。这就是错误推测法。
错误推测法的基本想法是:列举出程序中所有可能有的错误和容易发生错误的特殊情况,根据它们选择测试用例。例如,在介绍单元测试时曾列出许多在模块中常见的错误,这些是单元测试经验的总结。此外,对于在程序中容易出错的情况,也有一些经验总结出来。例如,输入数据为0,或输出数据为0是容易发生错误的情形,因此可选择输入数据为0,或使输出数据为0的例子作为测试用例。又例如,输入表格为空或输入表格只有一行,也是容易发生错误的情况。可选择表示这种情况的例子作为测试用例。再例如,可以针对一个排序程序,输入空的值(没有数据)、输入一个数据、让所有的输入数据都相等、让所有输入数据有序排列、让所有输入数据逆序排列等,进行错误推测。
前面介绍的等价类划分方法和边界值分析方法,都是着重考虑输入条件,但未考虑输入条件之间的联系。如果在测试时必须考虑输入条件的各种组合,可能的组合数将是天文数字。因此必须考虑使用一种适合于描述对于多种条件的组合,相应产生多个动作的形式来考虑设计测试用例,这就需要利用因果图。
因果图方法最终生成的就是判定表。它适合于检查程序输入条件的各种组合情况。
利用因果图生成测试用例的基本步骤是:
§ 分析软件规格说明描述中,哪些是原因(即输入条件或输入条件的等价类),哪些是结果(即输出条件),并给每个原因和结果赋予一个标识符。
§ 分析软件规格说明描述中的语义,找出原因与结果之间,原因与原因之间对应的是什么关系? 根据这些关系,画出因果图。
§ 由于语法或环境限制,有些原因与原因之间,原因与结果之间的组合情况不可能出现。为表明这些特殊情况,在因果图上用一些记号标明约束或限制条件。
§ 把因果图转换成判定表。
§ 把判定表的每一列拿出来作为依据,设计测试用例。
通常,在因果图中,用Ci表示原因,Ei表示结果,其基本符号如图7所示。各结点表示状态,可取值“0”或“1”。“0”表示某状态不出现,“1”表示某状态出现。
§ 恒等:若原因出现,则结果出现。若原因不出现,则结果也不出现。
§ 非:若原因出现,则结果不出现。若原因不出现,反而结果出现。
§ 或(∨):若几个原因中有一个出现,则结果出现,几个原因都不出现,结果不出现。
§ 与(∧):若几个原因都出现,结果才出现。若其中有一个原因不出现,结果不出现。
图7 因果图的图形符号
为了表示原因与原因之间,结果与结果之间可能存在的约束条件,在因果图中可以附加一些表示约束条件的符号。从输入(原因)考虑,有四种约束;从输出(结果)考虑,还有一种约束,参看图8:
§ E(互斥):表示a,b两个原因不会同时成立,两个中最多有一个可能成立。
§ I(包含):表示a,b,c三个原因中至少有一个必须成立。
§ O(唯一):表示a和b当中必须有一个,且仅有一个成立。
§ R(要求):表示当a出现时,b必须也出现。不可能a出现,b不出现。
§ M(屏蔽):表示当a是1时,b必须是0。而当a为0时,b的值不定。
图8 因果图的约束符号
因果图方法是一个非常有效的黑盒测试方法,它能够生成没有重复性的且发现错误能力强的测试用例,而且对输入、输出同时进行了分析。
Myers提出了使用各种测试方法的综合策略:
§ 在任何情况下都必须使用边界值分析方法。经验表明用这种方法设计出测试用例发现程序错误的能力最强。
§ 必要时用等价类划分方法补充一些测试用例。
§ 用错误推测法再追加一些测试用例。
§ 对照程序逻辑,检查已设计出的测试用例的逻辑覆盖程度。如果没有达到要求的覆盖标准,应当再补充足够的测试用例。
§ 如果程序的功能说明中含有输入条件的组合情况,则一开始就可选用因果图法。
通常采用以下一些方法进行源程序的静态分析。
① 生成各种引用表
§ 直接从表中查出说明/使用错误等。如,循环层次表、变量交叉引用表、标号交叉引用表等。
§ 为用户提供辅助信息。如,子程序(宏、函数)引用表、等价(变量、标号)表、常数表等。
§ 用来做错误预测和程序复杂度计算。如,操作符和操作数的统计表等。
② 静态错误分析
静态错误分析主要用于确定在源程序中是否有某类错误或“危险”结构。
§ 类型和单位分析 :为了强化对源程序中数据类型的检查,发现在数据类型上的错误和单位上的不一致性,在程序设计语言中扩充了一些结构。如单位分析要求使用一种预处理器,它能够通过使用一般的组合/消去规则,确定表达式的单位。
§ 引用分析 :最广泛使用的静态错误分析方法就是发现引用异常。如果沿着程序的控制路径,变量在赋值以前被引用,或变数在赋值以后未被引用,这时就发生了引用异常。为了检测引用异常,需要检查通过程序的每一条路径。也可以建立引用异常的探测工具。
§ 表达式分析 :对表达式进行分析,以发现和纠正在表达式中出现的错误。包括:在表达式中不正确地使用了括号造成错误。数组下标越界造成错误。除式为零造成错误。对负数开平方,或对π求正切值造成错误。以及对浮点数计算的误差进行检查。
§ 界面分析 :关于接口的静态错误分析主要检查过程、函数过程之间接口的一致性。因此要检查形参与实参在类型、数量、维数、顺序、使用上的一致性;检查全局变量和公共数据区在使用上的一致性。
静态分析中进行人工测试的主要方法有桌前检查、代码审查和走查。经验表明,使用这种方法能够有效地发现30%到70%的逻辑设计和编码错误。
① 桌前检查(Desk Checking)
由程序员自己检查自己编写的程序。程序员在程序通过编译之后,进行单元测试设计之前,对源程序代码进行分析,检验,并补充相关的文文件,目的是发现程序中的错误。检查项目有:
§ 检查变数的交叉引用表 :重点是检查未说明的变量和违反了类型规定的变量;还要对照源程序,逐个检查变量的引用、变量的使用序列;临时变量在某条路径上的重写情况;局部变量、全局变量与特权变量的使用;
§ 检查标号的交叉引用表 :验证所有标号的正确性:检查所有标号的命名是否正确;转向指定位置的标号是否正确。
§ 检查子程序、宏、函数 :验证每次调用与被调用位置是否正确;确认每次被调用的子程序、宏、函数是否存在;检验调用序列中调用方式与参数顺序、个数、类型上的一致性。
§ 等值性检查 :检查全部等价变量的类型的一致性,解释所包含的类型差异。
§ 常量检查 :确认每个常量的取值和数制、数据类型;检查常量每次引用同它的取值、数制和类型的一致性;
§ 标准检查 :用标准检查程序或手工检查程序中违反标准的问题。
§ 风格检查 :检查在程序设计风格方面发现的问题。
§ 比较控制流 :比较由程序员设计的控制流图和由实际程序生成的控制流图,寻找和解释每个差异,修改文档和校正错误。
§ 选择、启动路径 :在程序员设计的控制流图上选择路径,再到实际的控制流图上启动这条路径。如果选择的路径在实际控制流图上不能启动,则源程序可能有错。用这种方法启动的路径集合应保证源程序模块的每行代码都被检查,即桌前检查应至少是语句覆盖。
§ 对照程序的规格说明,详细阅读源代码 :程序员对照程序的规格说明书、规定的算法和程序设计语言的语法规则,仔细地阅读源代码,逐字逐句进行分析和思考,比较实际的代码和期望的代码,从它们的差异中发现程序的问题和错误。
§ 补充文档 :桌前检查的文档是一种过渡性的文档,不是公开的正式文档。通过编写文文件,也是对程序的一种下意识的检查和测试,可以帮助程序员发现和抓住更多的错误。
这种桌前检查,由于程序员熟悉自己的程序和自身的程序设计风格,可以节省很多的检查时间,但应避免主观片面性。
② 代码会审(Code Reading Review)
代码会审是由若干程序员和测试员组成一个会审小组,通过阅读、讨论和争议,对程序进行静态分析的过程。
代码会审分两步:第一步,小组负责人提前把设计规格说明书、控制流程图、程序文本及有关要求、规范等分发给小组成员,作为评审的依据。小组成员在充分阅读这些材料之后,进入审查的第二步:召开程序审查会。在会上,首先由程序员逐句讲解程序的逻辑。在此过程中,程序员或其它小组成员可以提出问题,展开讨论,审查错误是否存在。实践表明,程序员在讲解过程中能发现许多原来自己没有发现的错误,而讨论和争议则促进了问题的暴露。
在会前,应当给会审小组每个成员准备一份常见错误的清单,把以往所有可能发生的常见错误罗列出来,供与会者对照检查,以提高会审的实效。这个常见错误清单也叫做检查表,它把程序中可能发生的各种错误进行分类,对每一类列举出尽可能多的典型错误,然后把它们制成表格,供在会审时使用。这种检查表类似于本章单元测试中给出的检查表。
③ 走查(Walkthroughs)
走查与代码会审基本相同,其过程分为两步。第一步也把材料先发给走查小组每个成员,让他们认真研究程序,然后再开会。开会的程序与代码会审不同,不是简单地读程序和对照错误检查表进行检查,而是让与会者“充当”计算机。即首先由测试组成员为被测程序准备一批有代表性的测试用例,提交给走查小组。走查小组开会,集体扮演计算机角色,让测试用例沿程序的逻辑运行一遍,随时记录程序的踪迹,供分析和讨论用。
人们借助于测试用例的媒介作用,对程序的逻辑和功能提出各种疑问,结合问题开展热烈的讨论和争议,能够发现更多的问题。
软件测试也是一个系统工程,在做测试时,需要先做测试计划和规格说明,然后设计测试用例,定义策略,最后将测试结果与预先给出的期望结果进行比较,再做评价分析。而软件调试则是在进行了成功的测试之后才开始的工作。它与软件测试不同,软件测试的目的是尽可能多地发现软件中的错误,但进一步诊断和改正程序中潜在的错误,则是调试的任务。
调试活动由两部分组成:
① 确定程序中可疑错误的确切性质和位置。
② 对程序(设计,编码)进行修改,排除这个错误。
通常,调试工作是一个具有很强技巧性的工作。一个软件工程人员在分析测试结果的时候会发现,软件运行失效或出现问题,往往只是潜在错误的外部表现,而外部表现与内在原因之间常常没有明显的联系。如果要找出真正的原因,排除潜在的错误,不是一件易事。因此可以说,调试是通过现象,找出原因的一个思维分析的过程。
① 从错误的外部表现形式入手,确定程序中出错位置;
② 研究有关部分的程序,找出错误的内在原因;
③ 修改设计和代码,以排除这个错误;
④ 重复进行暴露了这个错误的原始测试或某些有关测试,以确认该错误是否被排除;是否引进了新的错误。
⑤ 如果所做的修正无效,则撤销这次改动,重复上述过程,直到找到一个有效的解决办法为止。
从技术角度来看,查找错误的难度在于:
§ 现象与原因所处的位置可能相距甚远。就是说,现象可能出现在程序的一个部位,而原因可能在离此很远的另一个位置。高耦合的程序结构中这种情况更为明显。
§ 当纠正其它错误时,这一错误所表现出的现象可能会暂时消失,但并未实际排除。
§ 现象实际上是由一些非错误原因(例如,舍入得不精确)引起的。
§ 现象可能是由于一些不容易发现的人为错误引起的。
§ 错误是由于时序问题引起的,与处理过程无关。
§ 现像是由于难于精确再现的输入状态(例如,实时应用中输入顺序不确定)引起。
§ 现象可能是周期出现的。在软、硬件结合的嵌入式系统中常常遇到。
调试的关键在于推断程序内部的错误位置及原因。为此,可以采用以下方法:
① 强行排错
这是目前使用较多,效率较低的调试方法。它不需要过多的思考,比较省脑筋。例如:
§ 通过内存全部打印来排错(Memory Dump);
§ 在程序特定部位设置打印语句;
§ 自动调试工具。
可供利用的典型的语言功能有:打印出语句执行的追踪信息,追踪子程序调用,以及指定变量的变化情况。自动调试工具的功能是:设置断点,当程序执行到某个特定的语句或某个特定的变量值改变时,程序暂停执行。程序员可在终端上观察程序此时的状态。
应用以上任一种方法之前,都应当对错误的征兆进行全面彻底的分析,得出对出错位置及错误性质的推测,再使用一种适当的排错方法来检验推测的正确性。
② 回溯法排错
这是在小程序中常用的一种有效的排错方法。一旦发现了错误,人们先分析错误征兆,确定最先发现“症状”的位置。然后,人工沿程序的控制流程,向回追踪源程序代码,直到找到错误根源或确定错误产生的范围。
回溯法对于小程序很有效,往往能把错误范围缩小到程序中的一小段代码;仔细分析这段代码不难确定出错的准确位置。但对于大程序,由于回溯的路径数目较多,回溯会变得很困难。
③ 归纳法排错
归纳法是一种从特殊推断一般的系统化思考方法。归纳法排错的基本思想是:从一些线索(错误征兆)着手,通过分析它们之间的关系来找出错误。
归纳法排错步骤大致分为以下四步:
§ 收集有关的资料 :列出所有已知的测试用例和程序执行结果。看哪些输入数据的运行结果是正确的,哪些输入数据的运行结果有错误存在。
§ 组织数据 :由于归纳法是从特殊到一般的推断过程,所以需要组织整理数据,以便发现规律。常用的构造线索的技术是“分类法”。
|
Yes |
No |
What(列出一般现象) |
|
|
Where(说明发现现象的地点) |
|
|
When(列出现象发生时所有已知情况) |
|
|
How(说明现象的范围和量级) |
|
|
而在“Yes”和“No”这两列中,“Yes”描述了出现错误的现象的3W1H,“No”作为比较,描述了没有错误的现象的3W1H。通过分析,找出矛盾来。
§ 提出假设 :分析线索之间的关系,利用在线索结构中观察到的矛盾现象,设计一个或多个关于出错原因的假设。如果一个假设也提不出来,归纳过程就需要收集更多的数据。此时,应当再设计与执行一些测试用例,以获得更多的资料。如果提出了许多假设,则首先选用最有可能成为出错原因的假设。
§ 证明假设 :把假设与原始线索或数据进行比较,若它能完全解释一切现象,则假设得到证明;否则,就认为假设不合理,或不完全,或是存在多个错误,以致只能消除部分错误。
④ 演绎法排错
演绎法是一种从一般原理或前提出发,经过排除和精化的过程来推导出结论的思考方法。演绎法排错是测试人员首先根据已有的测试用例,设想及枚举出所有可能出错的原因做为假设;然后再用原始测试数据或新的测试,从中逐个排除不可能正确的假设;最后,再用测试数据验证余下的假设确是出错的原因。
演绎法主要有以下四个步骤:
§ 列举所有可能出错原因的假设 :把所有可能的错误原因列成表。它们不需要完全的解释,而仅仅是一些可能因素的假设。通过它们,可以组织、分析现有数据。
§ 利用已有的测试数据,排除不正确的假设 :仔细分析已有的数据,寻找矛盾,力求排除前一步列出所有原因。如果所有原因都被排除了,则需要补充一些数据(测试用例),以建立新的假设;如果保留下来的假设多于一个,则选择可能性最大的原因做基本的假设。
§ 改进余下的假设 :利用已知的线索,进一步改进余下的假设,使之更具体化,以便可以精确地确定出错位置。
§ 证明余下的假设 :这一步极端重要,具体做法与归纳法的第4步相同。
在调试方面,许多原则本质上是心理学方面的问题。因为调试由两部分组成,所以调试原则也分成两组。
① 确定错误的性质和位置的原则
§ 用头脑去分析思考与错误征兆有关的信息。最有效的调试方法是用头脑分析与错误征兆有关的信息。一个能干的程序调试员应能做到不使用计算机就能够确定大部分错误。
§ 避开死胡同。如果程序调试员走进了死胡同,或者陷入了绝境,最好暂时把问题抛开,留到第二天再去考虑,或者向其它人讲解这个问题。事实上常有这种情形:向一个好的听众简单地描述这个问题时,不需要任何听讲者的提示,你自己会突然发现问题的所在。
§ 只把调试工具当做辅助手段来使用。利用调试工具,可以帮助思考,但不能代替思考。因为调试工具给你的是一种无规律的调试方法。实验证明,即使是对一个不熟悉的程序进行调试时,不用工具的人往往比使用工具的人更容易成功。
§ 避免用试探法,最多只能把它当做最后手段。初学调试的人最常犯的一个错误是想试试修改程序来解决问题。这还是一种碰运气的盲目的动作,它的成功机会很小,而且还常把新的错误带到问题中来。
② 修改错误的原则
§ 在出现错误的地方,很可能还有别的错误。经验证明,错误有群集现象,当在某一程序段发现有错误时,在该程序段中还存在别的错误的概率也很高。因此,在修改一个错误时,还要查一下它的近邻,看是否还有别的错误。
§ 修改错误的一个常见失误是只修改了这个错误的征兆或这个错误的表现,而没有修改错误的本身。如果提出的修改不能解释与这个错误有关的全部线索,那就表明了只修改了错误的一部分。
§ 当心修正一个错误的同时有可能会引入新的错误。人们不仅需要注意不正确的修改,而且还要注意看起来是正确的修改可能会带来的副作用,即引进新的错误。因此在修改了错误之后,必须进行回归测试,以确认是否引进了新的错误。
§ 修改错误的过程将迫使人们暂时回到程序设计阶段。修改错误也是程序设计的一种形式。一般说来,在程序设计阶段所使用的任何方法都可以应用到错误修正的过程中来。
§ 修改源代码程序,不要改变目标代码。
在软件开发的过程中,利用测试的统计数据,估算软件的可靠性,以控制软件的质量是至关重要的。
估算错误产生频度的一种方法是估算平均失效等待时间MTTF(Mean Time To Failure)。MTTF估算公式(Shooman模型)是
其中,K 是一个经验常数,美国一些统计数字表明,K的典型值是200;
ET 是测试之前程序中原有的故障总数;
IT 是程序长度(机器指令条数或简单汇编语句条数);
t是测试(包括排错)的时间;
EC (t) 是在0~t期间内检出并排除的故障总数。
公式的基本假定是:
§ 单位元(程序)长度中的故障数ET∕IT近似为常数,它不因测试与排错而改变。 统计数字表明,通常ET∕IT 值的变化范围在0.5×10-2~2×10-2之间;
§ 故障检出率正比于程序中残留故障数,而MTTF与程序中残留故障数成正比;
§ 故障不可能完全检出,但一经检出立即得到改正。
下面对此问题做一分析:
设EC (τ) 是0~τ时间内检出并排除的故障总数,τ是测试时间(月),则在同一段时间0~τ内的单条指令累积规范化排除故障数曲线εc (τ) 为:
εc (τ) = EC (τ)∕IT
这条曲线在开始呈递增趋势,然后逐渐和缓,最后趋近于一水平的渐近线ET∕IT。利用公式的基本假定:故障检出率(排错率)正比于程序中残留故障数及残留故障数必须大于零,经过推导得:
这就是故障累积的S型曲线模型,参看图11。
图11 故障累积曲线与故障检出曲线
故障检出曲线服从指数分布,亦在图11中显示。
①利用Shooman模型估算程序中原来错误总量ET —瞬间估算所以,
若设T是软件总的运行时间,M是软件在这段时间内的故障次数,则
T∕M = 1∕λ= MTTF
现在对程序进行两次不同的互相独立的功能测试,相应检错时间τ1 <τ2,检出的错误数EC (τ1 ) < EC (τ2 ),则有且解上述方程组,得到ET的估计值和K的估计值。
② 利用植入故障法估算程序中原有故障总数ET ─ 捕获-再捕获抽样法
若设NS是在测试前人为地向程序中植入的故障数(称播种故障),nS是经过一段时间测试后发现的播种故障的数目,nO是在测试中又发现的程序原有故障数。设测试用例发现植入故障和原有故障的能力相同,则程序中原有故障总数ET的估算值为
在此方法中要求对播种故障和原有故障同等对待,因此可以由对这些植入的已知故障一无所知的测试专业小组进行测试。
这种对播种故障的捕获─再捕获的抽样方法显然需要消耗许多时间在发现和修改播种故障上,这会影响工程的进度,而且要想使植入的故障有利于精确地推测原有的故障数,如何选择和植入这些播种故障也是一件很困难的事情。为了回避这些难点,就有了下面不必埋设播种故障的方法。
③ Hyman分别测试法
这是对植入故障法的一种补充。由两个测试员同时互相独立地测试同一程序的两个副本,用t表示测试时间(月),记t = 0时,程序中原有故障总数是B0;t = t1时,测试员甲发现的故障总数是B1;测试员乙发现的故障总数是B2;其中两人发现的相同故障数目是bc;两人发现的不同故障数目是bi。
在大程序测试时,头几个月所发现的错误在总的错误中具有代表性,两个测试员测试的结果应当比较接近,bi不是很大。这时有
如果bi比较显著,应当每隔一段时间,由两个测试员再进行分别测试,分析测试结果,估算B0。如果bi减小,或几次估算值的结果相差不多,则可用B0作为程序中原有错误总数ET的估算值。