第五章 软件测试 一、复习要求 1. 了解软件测试的目的和原则。 2. 了解软件错误的分类。 3. 了解软件测试的过程和策略。 4. 了解软件测试用例设计的方法,掌握逻辑覆盖、基本路径测试、因果图等测试用例设计方法。 5. 了解程序静态测试的方法。 6. 了解程序调试的概念。 7. 掌握软件测试中的可靠性分析方法 二、内容提要 1. 软件测试基础 (1) 什么是软件测试 软件测试是为了发现错误而执行程序的过程。或者说,软件测试是根据软件开发各阶段的规格说明和程序的内部结构而精心设计一批测试用例(即输入数据及其预期的输出结果),并利用这些测试用例去运行程序,以发现程序错误的过程。 软件测试在软件生存期中横跨两个阶段:通常在编写出每一个模块之后就对它做必要的测试(称为单元测试)。模块的编写者与测试者是同一个人。编码与单元测试属于软件生存期中的同一个阶段。在这个阶段结束之后,对软件系统还要进行各种综合测试,这是软件生存期的另一个独立的阶段,即测试阶段,通常由专门的测试人员承担这项工作。 (2) 软件测试的目的和原则 Grenford J.Myers就软件测试目的提出以下观点: ( 测试是程序的执行过程,目的在于发现错误; ( 一个好的测试用例在于能发现至今未发现的错误; ( 一个成功的测试是发现了至今未发现的错误的测试。 设计测试的目标是想以最少的时间和人力系统地找出软件中潜在的各种错误和缺陷。如果我们成功地实施了测试,就能够发现软件中的错误。测试的附带收获是,它能够证明软件的功能和性能与需求说明相符合。此外,实施测试收集到的测试结果数据为可靠性分析提供了依据。 测试不能表明软件中不存在错误,它只能说明软件中存在错误。 软件测试的原则: ① 应当把“尽早地和不断地进行软件测试”作为软件开发者的座右铭。 不应把软件测试仅仅看作是软件开发的一个独立阶段,而应当把它贯穿到软件开发的各个阶段中。坚持在软件开发的各个阶段的技术评审,这样才能在开发过程中尽早发现和预防错误,把出现的错误克服在早期,杜绝某些发生错误的隐患。 ② 测试用例应由测试输入数据和与之对应的预期输出结果这两部分组成。 测试以前应当根据测试的要求选择测试用例(Test case),用来检验程序员编制的程序,因此不但需要测试的输入数据,而且需要针对这些输入数据的预期输出结果。 ③ 程序员应避免检查自己的程序。 程序员应尽可能避免测试自己编写的程序,程序开发小组也应尽可能避免测试本小组开发的程序。如果条件允许,最好建立独立的软件测试小组或测试机构。这点不能与程序的调试(debuging)相混淆。调试由程序员自己来做可能更有效。 ④ 在设计测试用例时,应当包括合理的输入条件和不合理的输入条件。 合理的输入条件是指能验证程序正确的输入条件,不合理的输入条件是指异常的,临界的,可能引起问题异变的输入条件。软件系统处理非法命令的能力必须在测试时受到检验。用不合理的输入条件测试程序时,往往比用合理的输入条件进行测试能发现更多的错误。 ⑤ 充分注意测试中的群集现象。 在被测程序段中,若发现错误数目多,则残存错误数目也比较多。这种错误群集性现象,已为许多程序的测试实践所证实。根据这个规律,应当对错误群集的程序段进行重点测试,以提高测试投资的效益。 ⑥ 严格执行测试计划,排除测试的随意性。 测试之前应仔细考虑测试的项目,对每一项测试做出周密的计划,包括被测程序的功能、输入和输出、测试内容、进度安排、资源要求、测试用例的选择、测试的控制方式和过程等,还要包括系统的组装方式、跟踪规程、调试规程,回归测试的规定,以及评价标准等。对于测试计划,要明确规定,不要随意解释。 ⑦ 应当对每一个测试结果做全面检查。 有些错误的征兆在输出实测结果时已经明显地出现了,但是如果不仔细地全面地检查测试结果,就会使这些错误被遗漏掉。所以必须对预期的输出结果明确定义,对实测的结果仔细分析检查,抓住征侯,暴露错误。 ⑧ 妥善保存测试计划,测试用例,出错统计和最终分析报告,为维护提供方便。 (3) 确认和验证的关系 确认(Validation)是一系列的活动和过程,其目的是想证实在一个给定的外部环境中软件的逻辑正确性。它包括需求规格说明的确认和程序的确认,而程序的确认又分为静态确认与动态确认。静态确认一般不在计算机上实际执行程序,而是通过人工分析或者程序正确性证明来确认程序的正确性; 动态确认主要通过动态分析和程序测试来检查程序的执行状态,以确认程序是否有问题。 验证(Verification),则试图证明在软件生存期各个阶段,以及阶段间的逻辑协调性、完备性和正确性。 确认与验证工作都属于软件测试。在对需求理解与表达的正确性、设计与表达的正确性、实现的正确性以及运行的正确性的验证中,任何一个环节上发生了问题都可能在软件测试中表现出来。 (4) 测试信息流 测试信息流如图5.1所示。测试过程需要三类输入: ( 软件配置:包括软件需求规格说明、软件设计规格说明、源代码等; ( 测试配置:包括测试计划、测试用例、测试驱动程序等; ( 测试工具:测试工具为测试的实施提供某种服务。例如,测试数据自动生成程序、静态分析程序、动态分析程序、测试结果分析程序、以及驱动测试的工作台等。 测试之后,用实测结果与预期结果进行比较。如果发现出错的数据,就要进行调试。对已经发现的错误进行错误定位和确定出错性质,并改正这些错误,同时修改相关的文档。修正后的文档一般都要经过再次测试,直到通过测试为止。 通过收集和分析测试结果数据,对软件建立可靠性模型。  图5.1 测试信息流 如果测试发现不了错误,那么可以肯定,测试配置考虑得不够细致充分,错误仍然潜伏在软件中。这些错误最终不得不由用户在使用中发现,并在维护时由开发者去改正。但那时改正错误的费用将比在开发阶段改正错误的费用要高出40倍到60倍。 (5) 测试与软件开发各阶段的关系 软件开发过程是一个自顶向下,逐步细化的过程,而测试过程则是依相反的顺序安排的 自底向上,逐步集成的过程。低一级测试为上一级测试准备条件。参看图5.2,首先对每一个程序模块进行单元测试,消除程序模块内部在逻辑上和功能上的错误和缺陷。再对照软件设计进行集成测试,检测和排除子系统(或系统)结构上的错误。随后再对照需求,进行确认测试。最后从系统全体出发,运行系统,看是否满足要求。  图5.2 软件测试与软件开发过程的关系 2. 程序错误分类 由于人们对错误有不同的理解和认识,所以目前还没有一个统一的错误分类方法。错误难于分类的原因,一方面是由于一个错误有许多征兆,因而它可以被归入不同的类。另一方面是因为把一个给定的错误归于哪一类,还与错误的来源和程序员的心理状态有关。 (1) 按错误的影响和后果分类 ( 较小错误:只对系统输出有一些非实质性影响。如,输出的数据格式不合要求等。 ( 中等错误:对系统的运行有局部影响。如输出的某些数据有错误或出现冗余。 ( 较严重错误:系统的行为因错误的干扰而出现明显不合情理的现象。比如开出了0.00元的支票,系统的输出完全不可信赖。 ( 严重错误:系统运行不可跟踪,一时不能掌握其规律,时好时坏。 ( 非常严重的错误:系统运行中突然停机,其原因不明,无法软启动。 ( 最严重的错误:系统运行导致环境破坏,或是造成事故,引起生命、财产的损失。 (2) 按错误的性质和范围分类 B.Beizer从软件测试观点出发,把软件错误分为5类。 ① 功能错误 ( 规格说明错误:规格说明可能不完全,有二义性或自身矛盾。 ( 功能错误:程序实现的功能与用户要求的不一致。这常常是由于规格说明中包含错误的功能、多余的功能或遗漏的功能所致。 ( 测试错误:软件测试的设计与实施发生错误。软件测试自身也可能发生错误。 ( 测试标准引起的错误:对软件测试的标准要选择适当,若测试标准太复杂,则导致测试过程出错的可能就大。 ② 系统错误 ( 外部接口错误:外部接口指如终端、打印机、通信线路等系统与外部环境通信的手段。所有外部接口之间,人与机器之间的通信都使用形式的或非形式的专门协议。如果协议有错,或太复杂,难以理解,致使在使用中出错。此外还包括对输入/输出格式错误理解,对输入数据不合理的容错等等。 ( 内部接口错误:内部接口指程序之间的联系。它所发生的错误与程序内实现的细节有关。例如,设计协议错、输入/输出格式错、数据保护不可靠、子程序访问错等。 ( 硬件结构错误:这类错误在于不能正确地理解硬件如何工作。例如,忽视或错误地理解分页机构、地址生成、通道容量、I/O指令、中断处理、设备初始化和启动等而导致的出错。 ( 操作系统错误:这类错误主要是由于不了解操作系统的工作机制而导致出错。。当然,操作系统本身也有错误,但是一般用户很难发现这种错误。 ( 软件结构错误:由于软件结构不合理或不清晰而引起的错误。这种错误通常与系统的负载有关,而且往往在系统满载时才出现。这是最难发现的一类错误。例如,错误地设置局部参数或全局参数;错误地假定寄存器与存储器单元初始化了;错误地假定不会发生中断而导致不能封锁或开中断;错误地假定程序可以绕过数据的内部锁而导致不能关闭或打开内部锁;错误地假定被调用子程序常驻内存或非常驻内存等等,都将导致软件出错。 ( 控制与顺序错误:这类错误包括:忽视了时间因素而破坏了事件的顺序;猜测事件出现在指定的序列中;等待一个不可能发生的条件;漏掉先决条件;规定错误的优先级或程序状态;漏掉处理步骤;存在不正确的处理步骤或多余的处理步骤等。 ( 资源管理错误:这类错误是由于不正确地使用资源而产生的。例如,使用未经获准的资源;使用后未释放资源;资源死锁;把资源链接在错误的队列中等等。 ③ 加工错误 ( 算术与操作错误:指在算术运算、函数求值和一般操作过程中发生的错误。包括:数据类型转换错;除法溢出;错误地使用关系比较符;用整数与浮点数做比较等。 ( 初始化错误:典型的错误有:忘记初始化工作区,忘记初始化寄存器和数据区;错误地对循环控制变量赋初值;用不正确的格式,数据或类型进行初始化等等。 ( 控制和次序错误:这类错误与系统级同名错误类似,但它是局部错误。包括:遗漏路径;不可达到的代码;不符合语法的循环嵌套;循环返回和终止的条件不正确;漏掉处理步骤或处理步骤有错等。 ( 静态逻辑错误:这类错误主要包括:不正确地使用CASE语句;在表达式中使用不正确的否定(例如用“>”代替“<”的否定);对情况不适当地分解与组合;混淆“或”与“异或”等。 ④ 数据错误 ( 动态数据错误:动态数据是在程序执行过程中暂时存在的数据。各种不同类型的动态数据在程序执行期间将共享一个共同的存储区域,若程序启动时对这个区域未初始化,就会导致数据出错。由于动态数据被破坏的位置可能与出错的位置在距离上相差很远,因此要发现这类错误比较困难。 ( 静态数据错误:静态数据在内容和格式上都是固定的。它们直接或间接地出现在程序或数据库中。由编译程序或其它专门程序对它们做预处理。这是在程序执行前防止静态错误的好办法,但预处理也会出错。 ( 数据内容错误:数据内容是指存储于存储单元或数据结构中的位串、字符串或数字。数据内容本身没有特定的含义,除非通过硬件或软件给予解释。数据内容错误就是由于内容被破坏或被错误地解释而造成的错误。 ( 数据结构错误:数据结构是指数据元素的大小和组织形式。在同一存储区域中可以定义不同的数据结构。数据结构错误主要包括结构说明错误及把一个数据结构误当做另一类数据结构使用的错误。这是更危险的错误。 ( 数据属性错误:数据属性是指数据内容的含义或语义。例如,整数、字符串、子程序等等。数据属性错误主要包括:对数据属性不正确地解释,比如错把整数当实数,允许不同类型数据混合运算而导致的错误等。 ⑤ 代码错误 主要包括:语法错误;打字错误;对语句或指令不正确理解所产生的错误。 (3) 按软件生存期阶段分类 Good enough-Gerhart分类方法把软件的逻辑错误按生存期不同阶段分为4类。 ① 问题定义(需求分析)错误 它们是在软件定义阶段,分析员研究用户的要求后所编写的文档中出现的错误。换句话说,这类错误是由于问题定义不满足用户的要求而导致的错误。 ② 规格说明错误 这类错误是指规格说明与问题定义不一致所产生的错误。它们又可以细分成: ( 不一致性错误:规格说明中功能说明与问题定义发生矛盾。 ( 冗余性错误:规格说明中某些功能说明与问题定义相比是多余的。 ( 不完整性错误:规格说明中缺少某些必要的功能说明。 ( 不可行错误:规格说明中有些功能要求是不可行的。 ( 不可测试错误:有些功能的测试要求是不现实的。 ③ 设计错误 这是在设计阶段产生的错误,它使系统的设计与需求规格说明中的功能说明不相符。它们又可以细分为: ( 设计不完全错误:某些功能没有被设计,或设计得不完全。 ( 算法错误:算法选择不合适。主要表现为算法的基本功能不满足功能要求、算法不可行或者算法的效率不符合要求。 ( 模块接口错误:模块结构不合理;模块与外部数据库的界面不一致,模块之间的界面不一致。 ( 控制逻辑错误:控制流程与规格说明不一致;控制结构不合理。 ( 数据结构错误:数据设计不合理;与算法不匹配;数据结构不满足规格说明要求。 ④ 编码错误 编码过程中的错误是多种多样的,大体可归为以下几种:数据说明错、数据使用错、计算错、比较错、控制流错、界面错、输入/输出错,及其它的错误。 在不同的开发阶段,错误的类型和表现形式是不同的,故应当采用不同的方法和策略来进行检测。 3. 软件测试的过程与策略 测试过程按4个步骤进行,即单元测试、组装测试、确认测试和系统测试。图5.3显示出软件测试经历的4个步骤。单元测试集中对用源代码实现的每一个程序单元进行测试,检查各个程序模块是否正确地实现了规定的功能。然后,进行集成测试,根据设计规定的软件体系结构,把已测试过的模块组装起来,在组装过程中,检查程序结构组装的正确性。确认测试则是要检查已实现的软件是否满足了需求规格说明中确定了的各种需求,以及软件配置是否完全、正确。最后是系统测试,把已经经过确认的软件纳入实际运行环境中,与其它系统成份组合在一起进行测试。严格地说,系统测试已超出了软件工程的范围。  图5.3 软件测试的过程 (1) 单元测试 单元测试针对程序模块,进行正确性检验的测试。其目的在于发现各模块内部可能存在的各种差错。单元测试需要从程序的内部结构出发设计测试用例。多个模块可以平行地独立进行单元测试。 ① 单元测试的内容 ( 模块接口测试 :对通过被测模块的数据流进行测试。为此,对模块接口,包括参数表、调用子模块的参数、全程数据、文件输入/输出操作都必须检查。 ( 局部数据结构测试 :设计测试用例检查数据类型说明、初始化、缺省值等方面的问题,还要查清全程数据对模块的影响。 ( 路径测试 : 选择适当的测试用例,对模块中重要的执行路径进行测试。对基本执行路径和循环进行测试可以发现大量的路径错误。 ( 错误处理测试 :检查模块的错误处理功能是否包含有错误或缺陷。例如,是否拒绝不合理的输入;出错的描述是否难以理解、是否对错误定位有误、是否出错原因报告有误、是否对错误条件的处理不正确;在对错误处理之前错误条件是否已经引起系统的干预等。 ( 边界测试 :要特别注意数据流、控制流中刚好等于、大于或小于确定的比较值时出错的可能性。对这些地方要仔细地选择测试用例,认真加以测试。 此外,如果对模块运行时间有要求的话,还要专门进行关键路径测试,以确定最坏情况下和平均意义下影响模块运行时间的因素。这类信息对进行性能评价是十分有用的。 ② 单元测试的步骤 通常单元测试在编码阶段进行。在源程序代码编制完成,经过评审和验证,确认没有语法错误之后,就开始进行单元测试的测试用例设计。利用设计文档,设计可以验证程序功能、找出程序错误的多个测试用例。对于每一组输入,应有预期的正确结果。 模块并不是一个独立的程序,在考虑测试模块时,同时要考虑它和外界的联系,用一些辅助模块去模拟与被测模块相联系的其它模块。这些辅助模块分为两种:  图5.4 单元测试的测试环境 ( 驱动模块:相当于被测模块的主程序。它接收测试数据,把这些数据传送给被测模块,最后输出实测结果。 ( 桩模块:用以代替被测模块调用的子模块。桩模块可以做少量的数据操作,不需要把子模块所有功能都带进来,但不允许什么事情也不做。 被测模块、与它相关的驱动模块及桩模块共同构成了一个“测试环境”,见图5.4。 如果一个模块要完成多种功能,且以程序包或对象类的形式出现,例如Ada中的包,MODULA中的模块,C++中的类。这时可以将这个模块看成由几个小程序组成。对其中的每个小程序先进行单元测试要做的工作,对关键模块还要做性能测试。对支持某些标准规程的程序,更要着手进行互联测试。有人把这种情况特别称为模块测试,以区别单元测试。 (2) 集成测试 在单元测试的基础上,需要将所有模块按照设计要求组装成为系统。这时需要考虑: ( 在把各个模块连接起来的时侯,穿越模块接口的数据是否会丢失; ( 一个模块的功能是否会对另一个模块的功能产生不利的影响; ( 各个子功能组合起来,能否达到预期要求的父功能; ( 全局数据结构是否有问题; ( 单个模块的误差累积起来,是否会放大,从而达到不能接受的程度。 ( 单个模块的错误是否会导致数据库错误。 选择什么方式把模块组装起来形成一个可运行的系统,直接影响到模块测试用例的形式、所用测试工具的类型、模块编号的次序和测试的次序、以及生成测试用例的费用和调试的费用。通常,把模块组装成为系统的方式有两种方式: ① 一次性集成方式 它是一种非增殖式集成方式。也叫做整体拼装。使用这种方式,首先对每个模块分别进行模块测试,然后再把所有模块组装在一起进行测试,最终得到要求的软件系统。 由于程序中不可避免地存在涉及模块间接口、全局数据结构等方面的问题,所以一次试运行成功的可能性并不很大。 ② 增殖式集成方式 又称渐增式集成方式。首先对一个个模块进行模块测试,然后将这些模块逐步组装成较大的系统,在组装的过程中边连接边测试,以发现连接过程中产生的问题。最后通过增殖逐步组装成为要求的软件系统。 ( 自顶向下的增殖方式:将模块按系统程序结构,沿控制层次自顶向下进行集成。由于这种增殖方式在测试过程中较早地验证了主要的控制和判断点。在一个功能划分合理的程序结构中,判断常出现在较高的层次,较早就能遇到。如果主要控制有问题,尽早发现它能够减少以后的返工。 ( 自底向上的增殖方式:从程序结构的最底层模块开始组装和测试。因为模块是自底向上进行组装,对于一个给定层次的模块,它的子模块(包括子模块的所有下属模块)已经组装并测试完成,所以不再需要桩模块。在模块的测试过程中需要从子模块得到的信息可以直接运行子模块得到。 ③ 混合增殖式测试:自顶向下增殖的方式和自底向上增殖的方式各有优缺点。自顶向下增殖方式的缺点是需要建立桩模块。要使桩模块能够模拟实际子模块的功能将是十分困难的。同时涉及复杂算法和真正输入/输出的模块一般在底层,它们是最容易出问题的模块,到组装和测试的后期才遇到这些模块,一旦发现问题,导致过多的回归测试。而自顶向下增殖方式的优点是能够较早地发现在主要控制方面的问题。自底向上增殖方式的缺点是“程序一直未能做为一个实体存在,直到最后一个模块加上去后才形成一个实体”。就是说,在自底向上组装和测试的过程中,对主要的控制直到最后才接触到。但这种方式的优点是不需要桩模块,而建立驱动模块一般比建立桩模块容易,同时由于涉及到复杂算法和真正输入/输出的模块最先得到组装和测试,可以把最容易出问题的部分在早期解决。此外自底向上增殖的方式可以实施多个模块的并行测试。 有鉴于此,通常是把以上两种方式结合起来进行组装和测试。 ( 衍变的自顶向下的增殖测试:它的基本思想是强化对输入/输出模块和引入新算法模块的测试,并自底向上组装成为功能相当完整且相对独立的子系统,然后由主模块开始自顶向下进行增殖测试。 ( 自底向上-自顶向下的增殖测试:它首先对含读操作的子系统自底向上直至根结点模块进行组装和测试,然后对含写操作的子系统做自顶向下的组装与测试。 ( 回归测试:这种方式采取自顶向下的方式测试被修改的模块及其子模块,然后将这一部分视为子系统,再自底向上测试,以检查该子系统与其上级模块的接口是否适配。 (3) 确认测试 确认测试又称有效性测试。它的任务是验证软件的有效性,即验证软件的功能和性能及其它特性是否与用户的要求一致。在软件需求规格说明书描述了全部用户可见的软件属性,其中有一节叫做有效性准则,它包含的信息就是软件确认测试的基础。 在确认测试阶段需要做的工作如图5.5所示。首先要进行有效性测试以及软件配置复审,然后进行验收测试和安装测试,在通过了专家鉴定之后,才能成为可交付的软件。  图5.5 确认测试的步骤 ① 进行有效性测试(功能测试) 有效性测试是在模拟的环境(可能就是开发的环境)下,运用黑盒测试的方法,验证被测软件是否满足需求规格说明书列出的需求。为此,需要首先制定测试计划,规定要做测试的种类。还需要制定一组测试步骤,描述具体的测试用例。通过实施预定的测试计划和测试步骤,确定软件的特性是否与需求相符,确保所有的软件功能需求都能得到满足,所有的软件性能需求都能达到,所有的文档都是正确且便于使用。同时,对其它软件需求,例如可移植性、兼容性、出错自动恢复、可维护性等,也都要进行测试,确认是否满足。 ② 软件配置复查 软件配置复查的目的是保证软件配置的所有成分都齐全,各方面的质量都符合要求,具有维护阶段所必需的细节,而且已经编排好分类的目录。 除了按合同规定的内容和要求,由人工审查软件配置之外,在确认测试的过程中,应当严格遵守用户手册和操作手册中规定的使用步骤,以便检查这些文档资料的完整性和正确性。必须仔细记录发现的遗漏和错误,并且适当地补充和改正。 ③ 验收测试 在通过了系统的有效性测试及软件配置审查之后,就应开始系统的验收测试。验收测试是以用户为主的测试。软件开发人员和QA(质量保证)人员也应参加。由用户参加设计测试用例,使用用户界面输入测试数据,并分析测试的输出结果。一般使用生产中的实际数据进行测试。在测试过程中,除了考虑软件的功能和性能外,还应对软件的可移植性、兼容性、可维护性、错误的恢复功能等进行确认。 ④ α测试和β测试 在软件交付使用之后,用户将如何实际使用程序,对于开发者来说是无法预测的。因为用户在使用过程中常常会发生对使用方法的误解、异常的数据组合、以及产生对某些用户来说似乎是清晰的但对另一些用户来说却难以理解的输出等等。 如果软件是为多个用户开发的产品的时侯,让每个用户逐个执行正式的验收测试是不切实际的。很多软件产品生产者采用一种称之为α测试和β测试的测试方法,以发现可能只有最终用户才能发现的错误。 α测试是由一个用户在开发环境下进行的测试,也可以是公司内部的用户在模拟实际操作环境下进行的测试。这是在受控制的环境下进行的测试。α测试的目的是评价软件产品的FURPS(即功能、可使用性、可靠性、性能和支持)。尤其注重产品的界面和特色。α测试人员是除开产品开发人员之外首先见到产品的人,他们提出的功能和修改意见是特别有价值的。α测试可以从软件产品编码结束之时开始,或在模块(子系统)测试完成之后开始,也可以在确认测试过程中产品达到一定的稳定和可靠程度之后再开始。有关的手册(草稿)等应事先准备好。 β测试是由软件的多个用户在一个或多个用户的实际使用环境下进行的测试。与α测试不同的是,开发者通常不在测试现场。因而,β测试是在开发者无法控制的环境下进行的软件现场应用。在β测试中,由用户记下遇到的所有问题,包括真实的以及主观认定的,定期向开发者报告,开发者在综合用户的报告之后,做出修改,最后将软件产品交付给全体用户使用。β测试主要衡量产品的FURPS。着重于产品的支持性,包括文档、客户培训和支持产品生产能力。只有当α测试达到一定的可靠程度时,才能开始β测试。由于它处在整个测试的最后阶段,不能指望这时发现主要问题。同时,产品的所有手册文本也应该在此阶段完全定稿。由于β测试的主要目标是测试可支持性,所以β测试应尽可能由主持产品发行的人员来管理。 (4) 系统测试 所谓系统测试,是将通过确认测试的软件,作为整个基于计算机系统的一个元素,与计算机硬件、外设、某些支持软件、数据和人员等其它系统元素结合在一起,在实际运行(使用)环境下,对计算机系统进行一系列的组装测试和确认测试。 系统测试的目的在于通过与系统的需求定义作比较,发现软件与系统定义不符合或与之矛盾的地方。系统测试的测试用例应根据需求分析规格说明来设计,并在实际使用环境下来运行。 4. 测试用例设计 (1) 测试方法概述 软件测试的种类大致可以分为人工测试和基于计算机的测试。而基于计算机的测试由可以分为白盒测试和黑盒测试。 ① 黑盒测试 根据软件产品的功能设计规格,在计算机上进行测试,以证实每个实现了的功能是否符合要求。这种测试方法就是黑盒测试。黑盒测试意味着测试要在软件的接口处进行。就是说,这种方法是把测试对象看做一个黑盒子,测试人员完全不考虑程序内部的逻辑结构和内部特性,只依据程序的需求分析规格说明,检查程序的功能是否符合它的功能说明。 用黑盒测试发现程序中的错误,必须在所有可能的输入条件和输出条件中确定测试数据,来检查程序是否都能产生正确的输出。 ② 白盒测试 根据软件产品的内部工作过程,在计算机上进行测试,以证实每种内部操作是否符合设计规格要求,所有内部成分是否已经过检查。这种测试方法就是白盒测试。白盒测试把测试对象看做一个打开的盒子,允许测试人员利用程序内部的逻辑结构及有关信息,设计或选择测试用例,对程序所有逻辑路径进行测试。通过在不同点检查程序的状态,确定实际的状态是否与预期的状态一致。 不论是黑盒测试,还是白盒测试,都不可能把所有可能的输入数据都拿来进行所谓的穷举测试。因为可能的测试输入数据数目往往达到天文数字。下面让我们看两个例子。 假设一个程序P有输入X和Y及输出Z,参看图5.6。在字长为32位的计算机上运行。如果X、Y只取整数,考虑把所有的X、Y值都做为测试数据,按黑盒测试方法进行穷举测试,力图全面、无遗漏地“挖掘”出程序中的所有错误。这样做可能采用的测试数据组(Xi, Yi)的最大可能数目为:232×232=264。如果程序P测试一组X、Y数据需要1毫秒,且一天工作24小时,一年工作365天,要完成264组测试,需要5亿年。  图5.7 白盒测试中的穷举测试 而对一个具有多重选择和循环嵌套的程序,不同的路径数目也可能是天文数字。设给出一个如图5.7所示的小程序的流程图,其中包括了一个执行达20次的循环。那么它所包含的不同执行路径数高达520(=1013)条,若要对它进行穷举测试,覆盖所有的路径。假使测试程序对每一条路径进行测试需要1毫秒,同样假定一天工作24小时,一年工作365 天, 那么要想把如图5.7所示的小程序的所有路径测试完,则需要3170年。 以上的分析表明,实行穷举测试,由于工作量过大,实施起来是不现实的。任何软件开发项目都要受到期限、费用、人力和机时等条件的限制,尽管为了充分揭露程序中所有隐藏错误,需要针对所有可能的数据进行测试,但事实告诉我们,这样做是不可能的。 软件工程的总目标是充分利用有限的人力、物力资源,高效率、高质量、低成本地完成软件开发项目。在测试阶段既然穷举测试不可行,为了节省时间和资源,提高测试效率,就必须要从数量极大的可用测试用例中精心地挑选少量的测试数据,使得采用这些测试数据能够达到最佳的测试效果,能够高效率地把隐藏的错误揭露出来。 (2) 逻辑覆盖 逻辑覆盖是以程序内部的逻辑结构为基础的设计测试用例的技术。属白盒测试。这一方法要求测试人员对程序的逻辑结构有清楚的了解,甚至要能掌握源程序的所有细节。由于覆盖测试的目标不同,逻辑覆盖又可分为:语句覆盖、判定覆盖、判定-条件覆盖、条件组合覆盖及路径覆盖。 ① 语句覆盖 :语句覆盖就是设计若干个测试用例,运行被测程序,使得每一可执行语句至少执行一次。这种覆盖又称为点覆盖,它使得程序中每个可执行语句都得到执行,但它是最弱的逻辑覆盖准,效果有限,必须与其它方法交互使用。 ② 判定覆盖 :判定覆盖就是设计若干个测试用例,运行被测程序,使得程序中每个判断的取真分支和取假分支至少经历一次。判定覆盖又称为分支覆盖。 判定覆盖只比语句覆盖稍强一些,但实际效果表明,只是判定覆盖,还不能保证一定能查出在判断的条件中存在的错误。因此,还需要更强的逻辑覆盖准则去检验判断内部条件。 ③ 条件覆盖 :条件覆盖就是设计若干个测试用例,运行被测程序,使得程序中每个判断的每个条件的可能取值至少执行一次。 条件覆盖深入到判定中的每个条件,但可能不能满足判定覆盖的要求。 ④ 判定-条件覆盖 :判定-条件覆盖就是设计足够的测试用例,使得判断中每个条件的所有可能取值至少执行一次,同时每个判断本身的所有可能判断结果至少执行一次。换言之,即是要求各个判断的所有可能的条件取值组合至少执行一次。 判定-条件覆盖有缺陷。从表面上来看,它测试了所有条件的取值。但是事实并非如此。往往某些条件掩盖了另一些条件。会遗漏某些条件取值错误的情况。为彻底地检查所有条件的取值,需要将判定语句中给出的复合条件表达式进行分解,形成由多个基本判定嵌套的流程图。这样就可以有效地检查所有的条件是否正确了。 ⑤ 多重条件覆盖 :多重条件覆盖就是设计足够的测试用例,运行被测程序,使得每个判断的所有可能的条件取值组合至少执行一次。 这是一种相当强的覆盖准则,可以有效地检查各种可能的条件取值的组合是否正确。它不但可覆盖所有条件的可能取值的组合,还可覆盖所有判断的可取分支,但可能有的路径会遗漏掉。测试还不完全。 ⑤ 路径测试 :路径测试就是设计足够的测试用例,覆盖程序中所有可能的路径。这是最强的覆盖准则。但在路径数目很大时,真正做到完全覆盖是很困难的,必须把覆盖路径数目压缩到一定限度。下面我们做一分析。 (3) 关于控制结构测试的一些讨论 ① 分支结构的路径数 当程序中判定多于一个时,形成的分支结构可以分为两类:嵌套型分支结构和连锁型分支结构。如图5.9所示。对于嵌套型分支结构,若有n个判定语句,则需要n+1个测试用例;但对连锁型分支结构,若有n个判定语句,则需要有2n个测试用例,去覆盖它的2n条路径。当n较大时将无法测试。 为减少测试用例的数目,可采用试验设计法,抽取部分路径进行测试。由于抽样服从均匀分布,因此,在假定各条路径的重要性相同,或暂不明确各条路径的重要性的情况下可以做到均匀抽样。如果明确了各条路径的重要性,还可以采取加权的办法,筛选掉部分路径,再用如下的措施进行抽样。具体步骤如下: ⅰ)设耦合型分支结构中有n个判定,计算满足关系式 n+1≤2m 的最小自然数m; ⅱ)设t = 2m,取正交表Lt,并利用它设计测试数据。 例如,一个耦合型分支结构中有三个判定语句P1,P2,P3。它全部路径是23=8 条。先计算3+1≤2m = t的t,得t = 4。取正交表L4,如图5.10 (a) 所示,把每一列当做一个判定,每一行当做可取的测试用例,则正交表L4最多可取三个判定,分别代之以P1,P2,P3。判定P1,P2,P3的取假分支和取真分支分别记作S1、S2;S3、S4;S5、S6,用各个判定的取假分支取代正交表L4中的“0”,用取真分支取代正交表中的“1”,就建立起一个测试路径矩阵,如图5.10 (b) 所示。这样,测试路径数目从23=8条减少到3+1=4条。  图5.10 (a) 正交表L4 图5.10 (b) 路径抽样矩阵 ② 条件测试的策略 程序中的条件分为简单条件和复合条件。简单条件是一个布尔变量或一个关系表达式(可加前缀NOT),复合条件由简单条件通过逻辑运算符(AND、OR、NOT)和括号连接而成。如果条件出错,至少是条件中某一成分有错。条件中可能的出错类型有:布尔运算符错、布尔变量错、布尔括号错、关系运算符错、算术表达式错。 如果在一个判定的复合条件表达式中每个布尔变量和关系运算符最多只出现一次,而且没有公共变量,应用一种称之为BRO(分支与关系运算符)的测试法可以发现多个布尔运算符或关系运算符错,以及其它错误。 BRO策略引入条件约束的概念。设有n个简单条件的复合条件C,其条件约束为D =(D1, D2, …, Dn),其中Di(1≤i≤n)是条件C中第i个简单条件的输出约束。如果在C的执行过程中,其每个简单条件的输出都满足D中对应的约束,则称条件C的条件约束D由C的执行所覆盖。特别地,布尔变量或布尔表达式的输出约束必须是真(t)或假(f);关系表达式的输出约束为符号>、=、<。 ( 设条件为 C1 : B1 & B2 其中B1、B2是布尔变量,C1的输出约束为(D1, D2),在此,D1和D2或为t或为f。则(t, f)是C1可能的一个约束。覆盖此约束的测试(一次运行)将令B1为t,B2为f。BRO策略要求对C1的可能约束集合 { ( t, t ), ( f, t ), ( t, f ) } 中的每一个,分别设计一组测试用例。如果布尔运算符有错,这三组测试用例的运行结果必有一组导致C1失败。 ( 设条件为C2 : B1 & ( E3 = E4 ) 其中B1是布尔表达式,E3和E4是算术表达式,C2 的输出约束为(D1, D2),在此,D1或为t或为f;D2则是 <、= 或 >。因此,只有D2与C1中D2的不同,可以修改C1的约束集合 { ( t, t ), ( f, t ), ( t, f ) },导出C2的约束集合。因为在 ( E3 = E4 ) 中,"t" 相当于 "=","f" 相当于 "<" 或 ">",则C2的约束集合为 { ( t, = ), ( f, = ), ( t, < ), ( t, > ) }。据此设计4组测试用例,检查C2中可能的布尔或关系运算符中的错误。 ( 设条件为C3 : ( E1 > E2 ) & ( E3 = E4 ) 其中E1、E2、E3、E4都是算术表达式,C3的输出约束为(D1, D2),在此,D1和D2的约束均为 <、=、>。C3 中只有D1与C2中的D1不同,可以修改C2的约束集合 { ( t, = ), ( f, = ), ( t, < ), ( t, > ) },导出C3的约束集合。因为在 ( E1 > E2 ) 中,"t" 相当于 ">","f" 相当于 "<" 或 "=",则C3的约束集合为 { ( >, = ), ( <, = ), ( =, = ), ( >, < ), ( >, > ) }。根据这个约束集合设计测试用例,就能够检测C3中的关系运算符中的错误。 ③ 循环测试 循环分为4种不同类型:简单循环、连锁循环、嵌套循环和非结构循环,见图5.11。  图5.11 循环的分类 对于简单循环,测试应包括以下几种。其中的 n 表示循环允许的最大次数。 ( 零次循环:从循环入口直接跳到循环出口。 ( 一次循环:查找循环初始值方面的错误。 ( 二次循环:检查在多次循环时才能暴露的错误。 ( m次循环:此时的m<n,也是检查在多次循环时才能暴露的错误。 ( 最大次数循环、比最大次数多一次的循环、比最大次数少一次的循环。 对于嵌套循环,不能将简单循环的测试方法简单地扩大到嵌套循环,因为可能的测试数目将随嵌套层次的增加呈几何倍数增长。这可能导致一个天文数字的测试数目。下面给出一种有助于减少测试数目的测试方法。 ( 除最内层循环外,从最内层循环开始,置所有其它层的循环为最小值; ( 对最内层循环做简单循环的全部测试。测试时保持所有外层循环的循环变量为最小值。另外,对越界值和非法值做类似的测试。 ( 逐步外推,对其外面一层循环进行测试。测试时保持所有外层循环的循环变量取最小值,所有其它嵌套内层循环的循环变量取“典型”值。 ( 反复进行,直到所有各层循环测试完毕。 ( 对全部各层循环同时取最小循环次数,或者同时取最大循环次数。对于后一种测试,由于测试量太大,需人为指定最大循环次数。 对于连锁循环,要区别两种情况。如果各个循环互相独立,则连锁循环可以用与简单循环相同的方法进行测试。例如,有两个循环处于连锁状态,则前一个循环的循环变量的值就可以做为后一个循环的初值。但如果几个循环不是互相独立的,则需要使用测试嵌套循环的办法来处理。 对于非结构循环,应该使用结构化程序设计方法重新设计测试用例。 (4) 基本路径测试 如果把覆盖的路径数压缩到一定限度内,例如,程序中的循环体只执行零次和一次,就成为基本路径测试。它是在程序控制流图的基础上,通过分析控制构造的环路复杂性,导出基本可执行路径集合,从而设计测试用例的方法。 设计出的测试用例要保证在测试中,程序的每一个可执行语句至少要执行一次。 ① 程序的控制流图 控制流图是描述程序控制流的一种图示方法。基本控制构造的图形符号如图5.12所示。符号○称为控制流图的一个结点,一组顺序处理框可以映射为一个单一的结点。控制流图中的箭头称为边,它表示了控制流的方向,在选择或多分支结构中分支的汇聚处,即使没有执行语句也应该有一个汇聚结点。边和结点圈定的区域叫做区域,当对区域计数时,图形外的区域也应记为一个区域。  图5.12 控制流图的各种图形符号 如果判定中的条件表达式是复合条件时,即条件表达式是由一个或多个逻辑运算符(OR,AND,NAND,NOR)连接的逻辑表达式,则需要改复合条件的判定为一系列只有单个条件的嵌套的判定。例如对应图5.13 (a) 的复合条件的判定,应该画成如图5.13 (b) 所示的控制流图。 条件语句 if a OR b 中条件a和条件b各有一个只有单个条件的判定结点。  图5.13 复合逻辑下的控制流图 ② 计算程序环路复杂性 进行程序的基本路径测试时,程序的环路复杂性给出了程序基本路径集合中的独立路径条数,这是确保程序中每个可执行语句至少执行一次所必需的测试用例数目的上界。 所谓独立路径,是指包括一组以前没有处理的语句或条件的一条路径。如在图5.14(b)所示的控制流图中,一组独立的路径是 path1:1 - 11 path2:1 - 2 - 3 - 4 - 5 - 10 - 1 - 11 path3:1 - 2 - 3 - 6 - 8 - 9 - 10 - 1 - 11 path4:1 - 2 - 3 - 6 - 7 - 9 - 10 - 1 - 11 路径path1,path2,path3,path4组成了图5.14 (b) 所示控制流图的一个基本路径集。只要设计出的测试用例能够确保这些基本路径的执行,就可以使得程序中的每个可执行语句至少执行一次,每个条件的取真和取假分支也能得到测试。基本路径集不是唯一的,对于给定的控制流图,可以得到不同的基本路径集。  (a) 程序流程图 (b) 控制流图 图5.14 程序流程图与对应的控制流图 通常环路复杂性可用以下三种方法求得。 ( 将环路复杂性定义为控制流图中的区域数。 ( 设E为控制流图的边数,N为图的结点数,则定义环路复杂性为 V(G)=E-N+2。 ( 若设P为控制流图中的判定结点数,则有 V(G)=P+1。 因为图5.14(b)所示控制流图有4个区域。其环路复杂性为4。 它是构成基本路径集的独立路径数的上界。可以据此得到应该设计的测试用例的数目。 ③ 导出测试用例 利用逻辑覆盖方法生成测试用例,确保基本路径集中每条路径的执行。 (5) 等价类划分 等价类划分是一种典型的黑盒测试方法。使用这一方法时,完全不考虑程序的内部结构,只依据程序的规格说明来设计测试用例。由于不可能用所有可以输入的数据来测试程序,而只能从全部可供输入的数据中选择一个子集进行测试。如何选择适当的子集,使其尽可能多地发现错误。解决的办法之一就是等价类划分。 首先把数目极多的输入数据(有效的和无效的)划分为若干等价类。所谓等价类是指某个输入域的子集合。在该子集合中,各个输入数据对于揭露程序中的错误都是等效的。并合理地假定:测试某等价类的代表值就等价于对这一类其它值的测试。因此,我们可以把全部输入数据合理划分为若干等价类,在每一个等价类中取一个数据做为测试的输入条件,就可用少量代表性测试数据,取得较好的测试效果。 等价类的划分有两种不同的情况: ( 有效等价类:是指对于程序规格说明来说,是合理的,有意义的输入数据构成的集合。利用它,可以检验程序是否实现了规格说明预先规定的功能和性能。 ( 无效等价类:是指对于程序规格说明来说,是不合理的,无意义的输入数据构成的集合。利用它,可以检查程序中功能和性能的实现是否有不符合规格说明要求的地方。 在设计测试用例时,要同时考虑有效等价类和无效等价类的设计。软件不能都只接收合理的数据,还要经受意外的考验,接受无效的或不合理的数据,这样获得的软件才能具有较高的可靠性。划分等价类的原则如下: ( 按区间划分:如果可能的输入数据属于一个取值范围或值的个数限制范围,则可以确立一个有效等价类和两个无效等价类。 ( 按数值划分:如果规定了输入数据的一组值,而且程序要对每个输入值分别进行处理。则可为每一个输入值确立一个有效等价类,此外针对这组值确立一个无效等价类,它是所有不允许的输入值的集合。 ( 按数值集合划分:如果可能的输入数据属于一个值的集合,或者须满足“必须如何”的条件,这时可确立一个有效等价类和一个无效等价类。 ( 按限制条件或规则划分:如果规定了输入数据必须遵守的规则或限制条件,则可以确立一个有效等价类(符合规则)和若干个无效等价类(从不同角度违反规则)。 ② 确立测试用例 在确立了等价类之后,建立等价类表,列出所有划分出的等价类:  再从划分出的等价类中按以下原则选择测试用例: ( 设计尽可能少的测试用例,覆盖所有的有效等价类; ( 针对每一个无效等价类,设计一个测试用例来覆盖它。 (6) 边界值分析 人们从长期的测试工作经验得知,大量的错误是发生在输入或输出范围的边界上,而不是在输入范围的内部。因此针对各种边界情况设计测试用例,可以查出更多的错误。 比如,在做三角形计算时,要输入三角形的三个边长:A、B和C。 我们应注意到这三个数值应当满足A>0、B>0、C>0、A+B>C、A+C>B、B+C>A,才能构成三角形。但如果把六个不等式中的任何一个大于号“>”错写成大于等于号“≥”,那就不能构成三角形。问题恰出现在容易被疏忽的边界附近。这里所说的边界是指,相当于输入等价类和输出等价类而言,稍高于其边界值及稍低于其边界值的一些特定情况。 使用边界值分析方法设计测试用例,首先应确定边界情况。通常输入等价类与输出等价类的边界,就是应着重测试的边界情况。应当选取正好等于,刚刚大于,或刚刚小于边界的值做为测试数据,而不是选取等价类中的典型值或任意值做为测试数据。 边界值分析方法是最有效的黑盒测试方法,但当边界情况很复杂的时候,要找出适当的测试用例还需针对问题的输入域、输出域边界,耐心细致地逐个考虑。 (7) 错误推测法 人们也可以靠经验和直觉推测程序中可能存在的各种错误,从而有针对性地编写检查这些错误的例子。这就是错误推测法。 错误推测法的基本想法是:列举出程序中所有可能有的错误和容易发生错误的特殊情况,根据它们选择测试用例。例如,在介绍单元测试时曾列出许多在模块中常见的错误,这些是单元测试经验的总结。此外,对于在程序中容易出错的情况,也有一些经验总结出来。例如,输入数据为0,或输出数据为0是容易发生错误的情形,因此可选择输入数据为0,或使输出数据为0的例子作为测试用例。又例如,输入表格为空或输入表格只有一行,也是容易发生错误的情况。可选择表示这种情况的例子作为测试用例。再例如,可以针对一个排序程序,输入空的值(没有数据)、输入一个数据、让所有的输入数据都相等、让所有输入数据有序排列、让所有输入数据逆序排列等,进行错误推测。 (8) 因果图 前面介绍的等价类划分方法和边界值分析方法,都是着重考虑输入条件,但未考虑输入条件之间的联系。如果在测试时必须考虑输入条件的各种组合,可能的组合数将是天文数字。因此必须考虑使用一种适合于描述对于多种条件的组合,相应产生多个动作的形式来考虑设计测试用例,这就需要利用因果图。 因果图方法最终生成的就是判定表。它适合于检查程序输入条件的各种组合情况。 利用因果图生成测试用例的基本步骤是: ( 分析软件规格说明描述中,哪些是原因(即输入条件或输入条件的等价类),哪些是结果(即输出条件),并给每个原因和结果赋予一个标识符。 ( 分析软件规格说明描述中的语义,找出原因与结果之间,原因与原因之间对应的是什么关系? 根据这些关系,画出因果图。 ( 由于语法或环境限制,有些原因与原因之间,原因与结果之间的组合情况不可能出现。为表明这些特殊情况,在因果图上用一些记号标明约束或限制条件。 ( 把因果图转换成判定表。 ( 把判定表的每一列拿出来作为依据,设计测试用例。 通常,在因果图中,用Ci表示原因,Ei表示结果,其基本符号如图5.15所示。各结点表示状态,可取值“0”或“1”。“0”表示某状态不出现,“1”表示某状态出现。 ( 恒等:若原因出现,则结果出现。若原因不出现,则结果也不出现。 ( 非:若原因出现,则结果不出现。若原因不出现,反而结果出现。 ( 或(∨):若几个原因中有一个出现,则结果出现,几个原因都不出现,结果不出现。 ( 与(∧):若几个原因都出现,结果才出现。若其中有一个原因不出现,结果不出现。  图5.15 因果图的图形符号 为了表示原因与原因之间,结果与结果之间可能存在的约束条件,在因果图中可以附加一些表示约束条件的符号。从输入(原因)考虑,有四种约束;从输出(结果)考虑,还有一种约束,参看图5.16: ( E(互斥):表示a,b两个原因不会同时成立,两个中最多有一个可能成立。 ( I(包含):表示a,b,c三个原因中至少有一个必须成立。 ( O(唯一):表示a和b当中必须有一个,且仅有一个成立。 ( R(要求):表示当a出现时,b必须也出现。不可能a出现,b不出现。 ( M(屏蔽):表示当a是1时,b必须是0。而当a为0时,b的值不定。  图5.16 因果图的约束符号 【例】有一个处理单价为5角钱的饮料的自动售货机软件测试用例的设计。 其规格说明如下:“若投入5角钱或1元钱的硬币,押下〖橙汁〗或〖啤酒〗的按钮,则相应的饮料就送出来。若售货机没有零钱找,则一个显示〖零钱找完〗的红灯亮,这时在投入1元硬币并押下按钮后,饮料不送出来而且1元硬币也退出来;若有零钱找,则显示〖零钱找完〗的红灯灭,在送出饮料的同时退还5角硬币。” ( 分析这一段说明,列出原因和结果 原因: 1.售货机有零钱找 3.投入5角硬币 5.押下啤酒按钮   2.投入1元硬币 4.押下橙汁按钮   结果: 21. 售货机〖零钱找完〗灯亮 24. 送出橙汁饮料   22. 退还1元硬币 25. 送出啤酒饮料   23. 退还5角硬币   ( 画出因果图,如图5.17所示。所有原因结点列在左边,所有结果结点列在右边。 建立两个中间结点,表示处理的中间状态。 中间结点: 11. 投入1元硬币且押下饮料按钮   12. 押下〖橙汁〗或〖啤酒〗的按钮   13. 应当找5角零钱并且售货机有零钱找   14. 钱已付清    图5.17 因果图 ( 由于 2 与 3 ,4 与 5 不能同时发生,分别加上约束条件E。 ( 转换成判定表:   图5.18 由因果图得到的判定表 在判定表中,阴影部分表示因违反约束条件的不可能出现的情况,删去。第16列与第32列因什么动作也没做,也删去。最后可根据剩下的16列作为确定测试用例的依据。 因果图方法是一个非常有效的黑盒测试方法,它能够生成没有重复性的且发现错误能力强的测试用例,而且对输入、输出同时进行了分析。 (9) 测试方法选择的综合策略 Myers提出了使用各种测试方法的综合策略: ( 在任何情况下都必须使用边界值分析方法。经验表明用这种方法设计出测试用例发现程序错误的能力最强。 ( 必要时用等价类划分方法补充一些测试用例。 ( 用错误推测法再追加一些测试用例。 ( 对照程序逻辑,检查已设计出的测试用例的逻辑覆盖程度。如果没有达到要求的覆盖标准,应当再补充足够的测试用例。 ( 如果程序的功能说明中含有输入条件的组合情况,则一开始就可选用因果图法。 5. 程序的静态测试 (1) 源程序静态分析 通常采用以下一些方法进行源程序的静态分析。 ① 生成各种引用表 ( 直接从表中查出说明/使用错误等。如,循环层次表、变量交叉引用表、标号交叉引用表等。 ( 为用户提供辅助信息。如,子程序(宏、函数)引用表、等价(变量、标号)表、常数表等。 ( 用来做错误预测和程序复杂度计算。如,操作符和操作数的统计表等。 ② 静态错误分析 静态错误分析主要用于确定在源程序中是否有某类错误或“危险”结构。 ( 类型和单位分析 :为了强化对源程序中数据类型的检查,发现在数据类型上的错误和单位上的不一致性,在程序设计语言中扩充了一些结构。如单位分析要求使用一种预处理器,它能够通过使用一般的组合/消去规则,确定表达式的单位。 ( 引用分析 :最广泛使用的静态错误分析方法就是发现引用异常。如果沿着程序的控制路径,变量在赋值以前被引用,或变量在赋值以后未被引用,这时就发生了引用异常。为了检测引用异常,需要检查通过程序的每一条路径。也可以建立引用异常的探测工具。 ( 表达式分析 :对表达式进行分析,以发现和纠正在表达式中出现的错误。包括:在表达式中不正确地使用了括号造成错误。数组下标越界造成错误。除式为零造成错误。对负数开平方,或对π求正切值造成错误。以及对浮点数计算的误差进行检查。 ( 接口分析 :关于接口的静态错误分析主要检查过程、函数过程之间接口的一致性。因此要检查形参与实参在类型、数量、维数、顺序、使用上的一致性;检查全局变量和公共数据区在使用上的一致性。 (2) 人工测试 静态分析中进行人工测试的主要方法有桌前检查、代码审查和走查。经验表明,使用这种方法能够有效地发现30%到70%的逻辑设计和编码错误。 ① 桌前检查(Desk Checking) 由程序员自己检查自己编写的程序。程序员在程序通过编译之后,进行单元测试设计之前,对源程序代码进行分析,检验,并补充相关的文档,目的是发现程序中的错误。检查项目有: ( 检查变量的交叉引用表 :重点是检查未说明的变量和违反了类型规定的变量;还要对照源程序,逐个检查变量的引用、变量的使用序列;临时变量在某条路径上的重写情况;局部变量、全局变量与特权变量的使用; ( 检查标号的交叉引用表 :验证所有标号的正确性:检查所有标号的命名是否正确;转向指定位置的标号是否正确。 ( 检查子程序、宏、函数 :验证每次调用与被调用位置是否正确;确认每次被调用的子程序、宏、函数是否存在;检验调用序列中调用方式与参数顺序、个数、类型上的一致性。 ( 等值性检查 :检查全部等价变量的类型的一致性,解释所包含的类型差异。 ( 常量检查 :确认每个常量的取值和数制、数据类型;检查常量每次引用同它的取值、数制和类型的一致性; ( 标准检查 :用标准检查程序或手工检查程序中违反标准的问题。 ( 风格检查 :检查在程序设计风格方面发现的问题。 ( 比较控制流 :比较由程序员设计的控制流图和由实际程序生成的控制流图,寻找和解释每个差异,修改文档和校正错误。 ( 选择、激活路径 :在程序员设计的控制流图上选择路径,再到实际的控制流图上激活这条路径。如果选择的路径在实际控制流图上不能激活,则源程序可能有错。用这种方法激活的路径集合应保证源程序模块的每行代码都被检查,即桌前检查应至少是语句覆盖。 ( 对照程序的规格说明,详细阅读源代码 :程序员对照程序的规格说明书、规定的算法和程序设计语言的语法规则,仔细地阅读源代码,逐字逐句进行分析和思考,比较实际的代码和期望的代码,从它们的差异中发现程序的问题和错误。 ( 补充文档 :桌前检查的文档是一种过渡性的文档,不是公开的正式文档。通过编写文档,也是对程序的一种下意识的检查和测试,可以帮助程序员发现和抓住更多的错误。 这种桌前检查,由于程序员熟悉自己的程序和自身的程序设计风格,可以节省很多的检查时间,但应避免主观片面性。 ② 代码会审(Code Reading Review) 代码会审是由若干程序员和测试员组成一个会审小组,通过阅读、讨论和争议,对程序进行静态分析的过程。 代码会审分两步:第一步,小组负责人提前把设计规格说明书、控制流程图、程序文本及有关要求、规范等分发给小组成员,作为评审的依据。小组成员在充分阅读这些材料之后,进入审查的第二步:召开程序审查会。在会上,首先由程序员逐句讲解程序的逻辑。在此过程中,程序员或其他小组成员可以提出问题,展开讨论,审查错误是否存在。实践表明,程序员在讲解过程中能发现许多原来自己没有发现的错误,而讨论和争议则促进了问题的暴露。 在会前,应当给会审小组每个成员准备一份常见错误的清单,把以往所有可能发生的常见错误罗列出来,供与会者对照检查,以提高会审的实效。这个常见错误清单也叫做检查表,它把程序中可能发生的各种错误进行分类,对每一类列举出尽可能多的典型错误,然后把它们制成表格,供在会审时使用。这种检查表类似于本章单元测试中给出的检查表。 ③ 走查(Walkthroughs) 走查与代码会审基本相同,其过程分为两步。第一步也把材料先发给走查小组每个成员,让他们认真研究程序,然后再开会。开会的程序与代码会审不同,不是简单地读程序和对照错误检查表进行检查,而是让与会者“充当”计算机。即首先由测试组成员为被测程序准备一批有代表性的测试用例,提交给走查小组。走查小组开会,集体扮演计算机角色,让测试用例沿程序的逻辑运行一遍,随时记录程序的踪迹,供分析和讨论用。 人们借助于测试用例的媒介作用,对程序的逻辑和功能提出各种疑问,结合问题开展热烈的讨论和争议,能够发现更多的问题。 6. 调试(Debug,排错) 软件测试也是一个系统工程,在做测试时,需要先做测试计划和规格说明,然后设计测试用例,定义策略,最后将测试结果与预先给出的期望结果进行比较,再做评价分析。而软件调试则是在进行了成功的测试之后才开始的工作。它与软件测试不同,软件测试的目的是尽可能多地发现软件中的错误,但进一步诊断和改正程序中潜在的错误,则是调试的任务。 调试活动由两部分组成: ① 确定程序中可疑错误的确切性质和位置。 ② 对程序(设计,编码)进行修改,排除这个错误。 通常,调试工作是一个具有很强技巧性的工作。一个软件工程人员在分析测试结果的时候会发现,软件运行失效或出现问题,往往只是潜在错误的外部表现,而外部表现与内在原因之间常常没有明显的联系。如果要找出真正的原因,排除潜在的错误,不是一件易事。因此可以说,调试是通过现象,找出原因的一个思维分析的过程。 (1) 调试的步骤 ① 从错误的外部表现形式入手,确定程序中出错位置; ② 研究有关部分的程序,找出错误的内在原因; ③ 修改设计和代码,以排除这个错误; ④ 重复进行暴露了这个错误的原始测试或某些有关测试,以确认该错误是否被排除;是否引进了新的错误。 ⑤ 如果所做的修正无效,则撤消这次改动,重复上述过程,直到找到一个有效的解决办法为止。 从技术角度来看,查找错误的难度在于: ( 现象与原因所处的位置可能相距甚远。就是说,现象可能出现在程序的一个部位,而原因可能在离此很远的另一个位置。高耦合的程序结构中这种情况更为明显。 ( 当纠正其它错误时,这一错误所表现出的现象可能会暂时消失,但并未实际排除。 ( 现象实际上是由一些非错误原因(例如,舍入得不精确)引起的。 ( 现象可能是由于一些不容易发现的人为错误引起的。 ( 错误是由于时序问题引起的,与处理过程无关。 ( 现象是由于难于精确再现的输入状态(例如,实时应用中输入顺序不确定)引起。 ( 现象可能是周期出现的。在软、硬件结合的嵌入式系统中常常遇到。 (2) 几种主要的调试方法 调试的关键在于推断程序内部的错误位置及原因。为此,可以采用以下方法: ① 强行排错 这是目前使用较多,效率较低的调试方法。它不需要过多的思考,比较省脑筋。例如: ( 通过内存全部打印来排错(Memory Dump); ( 在程序特定部位设置打印语句; ( 自动调试工具。 可供利用的典型的语言功能有:打印出语句执行的追踪信息,追踪子程序调用,以及指定变量的变化情况。自动调试工具的功能是:设置断点,当程序执行到某个特定的语句或某个特定的变量值改变时,程序暂停执行。程序员可在终端上观察程序此时的状态。 应用以上任一种方法之前,都应当对错误的征兆进行全面彻底的分析,得出对出错位置及错误性质的推测,再使用一种适当的排错方法来检验推测的正确性。 ② 回溯法排错 这是在小程序中常用的一种有效的排错方法。一旦发现了错误,人们先分析错误征兆,确定最先发现“症状”的位置。然后,人工沿程序的控制流程,向回追踪源程序代码,直到找到错误根源或确定错误产生的范围。 回溯法对于小程序很有效,往往能把错误范围缩小到程序中的一小段代码;仔细分析这段代码不难确定出错的准确位置。但对于大程序,由于回溯的路径数目较多,回溯会变得很困难。 ③ 归纳法排错 归纳法是一种从特殊推断一般的系统化思考方法。归纳法排错的基本思想是:从一些线索(错误征兆)着手,通过分析它们之间的关系来找出错误。 归纳法排错步骤大致分为以下四步: ( 收集有关的数据 :列出所有已知的测试用例和程序执行结果。看哪些输入数据的运行结果是正确的,哪些输入数据的运行结果有错误存在。 ( 组织数据 :由于归纳法是从特殊到一般的推断过程,所以需要组织整理数据,以便发现规律。常用的构造线索的技术是“分类法”。 Yes No  What(列出一般现象)    Where(说明发现现象的地点)    When(列出现象发生时所有已知情况)    How(说明现象的范围和量级)    而在“Yes”和“No”这两列中,“Yes”描述了出现错误的现象的3W1H,“No”作为比较,描述了没有错误的现象的3W1H。通过分析,找出矛盾来。 ( 提出假设 :分析线索之间的关系,利用在线索结构中观察到的矛盾现象,设计一个或多个关于出错原因的假设。如果一个假设也提不出来,归纳过程就需要收集更多的数据。此时,应当再设计与执行一些测试用例,以获得更多的数据。如果提出了许多假设,则首先选用最有可能成为出错原因的假设。 ( 证明假设 :把假设与原始线索或数据进行比较,若它能完全解释一切现象,则假设得到证明;否则,就认为假设不合理,或不完全,或是存在多个错误,以致只能消除部分错误。 ④ 演绎法排错 演绎法是一种从一般原理或前提出发,经过排除和精化的过程来推导出结论的思考方法。演绎法排错是测试人员首先根据已有的测试用例,设想及枚举出所有可能出错的原因做为假设;然后再用原始测试数据或新的测试,从中逐个排除不可能正确的假设;最后,再用测试数据验证余下的假设确是出错的原因。 演绎法主要有以下四个步骤: ( 列举所有可能出错原因的假设 :把所有可能的错误原因列成表。它们不需要完全的解释,而仅仅是一些可能因素的假设。通过它们,可以组织、分析现有数据。 ( 利用已有的测试数据,排除不正确的假设 :仔细分析已有的数据,寻找矛盾,力求排除前一步列出所有原因。如果所有原因都被排除了,则需要补充一些数据(测试用例),以建立新的假设;如果保留下来的假设多于一个,则选择可能性最大的原因做基本的假设。 ( 改进余下的假设 :利用已知的线索,进一步改进余下的假设,使之更具体化,以便可以精确地确定出错位置。 ( 证明余下的假设 :这一步极端重要,具体做法与归纳法的第4步相同。 (3) 调试原则 在调试方面,许多原则本质上是心理学方面的问题。因为调试由两部分组成,所以调试原则也分成两组。 ① 确定错误的性质和位置的原则 ( 用头脑去分析思考与错误征兆有关的信息。最有效的调试方法是用头脑分析与错误征兆有关的信息。一个能干的程序调试员应能做到不使用计算机就能够确定大部分错误。 ( 避开死胡同。如果程序调试员走进了死胡同,或者陷入了绝境,最好暂时把问题抛开,留到第二天再去考虑,或者向其他人讲解这个问题。事实上常有这种情形:向一个好的听众简单地描述这个问题时,不需要任何听讲者的提示,你自己会突然发现问题的所在。 ( 只把调试工具当做辅助手段来使用。利用调试工具,可以帮助思考,但不能代替思考。因为调试工具给你的是一种无规律的调试方法。实验证明,即使是对一个不熟悉的程序进行调试时,不用工具的人往往比使用工具的人更容易成功。 ( 避免用试探法,最多只能把它当做最后手段。初学调试的人最常犯的一个错误是想试试修改程序来解决问题。这还是一种碰运气的盲目的动作,它的成功机会很小,而且还常把新的错误带到问题中来。 ② 修改错误的原则 ( 在出现错误的地方,很可能还有别的错误。经验证明,错误有群集现象,当在某一程序段发现有错误时,在该程序段中还存在别的错误的概率也很高。因此,在修改一个错误时,还要查一下它的近邻,看是否还有别的错误。 ( 修改错误的一个常见失误是只修改了这个错误的征兆或这个错误的表现,而没有修改错误的本身。如果提出的修改不能解释与这个错误有关的全部线索,那就表明了只修改了错误的一部分。 ( 当心修正一个错误的同时有可能会引入新的错误。人们不仅需要注意不正确的修改,而且还要注意看起来是正确的修改可能会带来的副作用,即引进新的错误。因此在修改了错误之后,必须进行回归测试,以确认是否引进了新的错误。 ( 修改错误的过程将迫使人们暂时回到程序设计阶段。修改错误也是程序设计的一种形式。一般说来,在程序设计阶段所使用的任何方法都可以应用到错误修正的过程中来。 ( 修改源代码程序,不要改变目标代码。 7. 测试中的可靠性分析 在软件开发的过程中,利用测试的统计数据,估算软件的可靠性,以控制软件的质量是至关重要的。 (1) 推测错误的产生频度 估算错误产生频度的一种方法是估算平均失效等待时间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型曲线模型,参看图5.19。  图5.19 故障累积曲线与故障检出曲线 故障检出曲线服从指数分布,亦在图5.19中显示。  (2) 估算软件中故障总数ET的方法 ①利用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的估算值。 三、例题分析 【例1】为了把握软件开发各个环节的正确性和协调性,人们需要进行( A )和( B )工作。( A )的目的是想证实在一给定的外部环境中软件的逻辑正确性。它包括( C )和( D ),( B )则试图证明在软件生存期各个阶段,以及阶段间的逻辑( E )、( F )和正确性。 供选择的答案: A, B. ① 操作 ② 确认 ③ 验证 ④ 测试 ⑤ 调试 C, D. ① 用户的确认 ② 需求规格说明的确认 ③ 程序的确认 ④ 测试的确认 E, F. ① 可靠性 ② 独立性 ③ 协调性 ④ 完备性 ⑤ 扩充性 答案:A. ② B. ③ C. ② D. ③ E. ③ F. ④ 分析:到程序的测试为止,软件开发工作已经经历了许多环节,每个环节都可能发生问题。为了把握各个环节的正确性,人们需要进行各种确认和验证工作。 所谓确认,是一系列的活动和过程,其目的是想证实在一个给定的外部环境中软件的逻辑正确性。它包括需求规格说明的确认和程序的确认,而程序的确认又分为静态确认与动态确认。静态确认一般不在计算机上实际执行程序,而是通过人工分析或者程序正确性证明来确认程序的正确性;动态确认主要通过动态分析和程序测试来检查程序的执行状态,以确认程序是否有问题。 所谓验证,则试图证明在软件生存期各个阶段,以及阶段间的逻辑协调性、完备性和正确性。下图中所示的就是软件生存期各个重要阶段之间所要保持的正确性。它们就是验证工作的主要对象。  确认与验证工作都属于软件测试。在对需求理解与表达的正确性、设计与表达的正确性、实现的正确性以及运行的正确性的验证中,任何一个环节上发生了问题都可能在软件测试中表现出来。 【例2】测试过程需要三类输入:( A )、( B )和( C )。请选择正确的答案填入下图中以完成测试信息处理的全过程。  供选择的答案: A ( C. ① 接口选择 ② 软件配置 ③ 硬件配置 ④ 测试配置 ⑤ 测试环境 ⑥ 测试工具 D ( F. ① 排错 ② 可靠性分析 ③ 结果分析 ④ 数据分类 答案:A. ② B. ④ C. ⑥ D. ③ E. ① F. ② 分析:测试信息流如图所示。测试过程需要三类输入: (1)软件配置:包括软件需求规格说明、软件设计规格说明、源代码等; (2)测试配置:包括表明测试工作如何进行的测试计划、给出测试数据的测试用例、控制测试进行的测试程序等;实际上,测试配置是软件配置的一个子集。 (3)测试工具:为提高软件测试效率,测试工作需要有测试工具的支持,它们的工作就是为测试的实施提供某种服务。例如,测试数据自动生成程序、静态分析程序、动态分析程序、测试结果分析程序、以及驱动测试的测试数据库等等。 测试之后,要对所有测试结果进行分析,即将实测的结果与预期的结果进行比较。如果发现出错的数据,就意味着软件有错误,然后就需要开始排错(调试)。即对已经发现的错误进行错误定位和确定出错性质,并改正这些错误,同时修改相关的文档。修正后的文档一般都要经过再次测试,直到通过测试为止。通过收集和分析测试结果数据,开始对软件建立可靠性模型。  测试信息流 最后,如果测试发现不了错误,那么几乎可以肯定,测试配置考虑得不够细致充分,错误仍然潜伏在软件中。这些错误最终不得不由用户在使用中发现,并在维护时由开发者去改正。但那时改正错误的费用将比在开发阶段改正错误的费用要高出40倍到60倍。 【例3】软件测试是软件质量保证的主要手段之一,测试的费用已超过( A )的30%以上。因此,提高测试的有效性十分重要。“高产”的测试是指( B )。根据国家标准GB 8566–88《计算机软件开发规范》的规定,软件的开发和维护划分为8个阶段,其中,单元测试是在( C )阶段完成的,集成测试的计划是在( D )阶段制定的,确认测试的计划是在( E )阶段制定的。 供选择的答案: A. ① 软件开发费用 ② 软件维护费用 ③ 软件开发和维护费用 ④ 软件研制费用 ⑤ 软件生存期全部 B. ① 用适量的测试用例运行程序,证明被测程序正确无误 ② 用适量的测试用例运行程序,证明被测程序符合相应的要求 ③ 用少量的测试用例运行程序,发现被测程序尽可能多的错误 ④ 用少量的测试用例运行程序,纠正被测程序尽可能多的错误 C ( E. ① 可行性研究和计划 ② 需求分析 ③ 概要设计 ④ 详细设计 ⑤ 实现 ⑥ 集成测试 ⑦ 确认测试 ⑧ 使用和维护 答案:A. ① B. ③ C. ⑤ D. ③ E. ② 分析:由于原始问题的复杂性,软件本身的复杂性和抽象性,软件开发各个阶段工作的多样性,以及参加开发各种层次人员之间工作的配合关系等因素,使得开发的每个环节都可能产生错误。这些错误和缺陷如果在软件交付投入生产性运行之前不能加以排除的话,在运行中迟早会暴露出来。但到那时,不仅改正这些错误的代价更高,而且往往造成很恶劣的后果。因此,为了保证所开发软件的质量,测试是不可少的。测试的任务就是设计出测试用例,运行被测程序,发现软件中隐藏的的各种错误。目前在大中型软件项目的开发中。测试占据着重要的地位,测试所花费费用已超过软件开发费用的70%以上。 如何组织好测试,特别是如何选择好的测试用例,对于降低测试费用,保障测试质量有着重要的意义。一个高效的、或“高产”的测试,是指设计少量的测试用例,用它们来运行程序,发现被测程序的尽可能多的问题。 测试按照测试的内容可分为三种:单元测试(对程序单元或模块单独进行测试)、集成测试(把已通过单元测试的模块按照设计的要求组装起来,测试模块间的接口以及设计中的问题)和确认测试(对软件的有效性做全面测试,以确定开发的软件是否符合验收标准)。 根据国标GB 8566–88《计算机软件开发规范》的规定,软件的开发和维护划分为8个阶段:可行性研究及计划、需求分析、概要设计、详细设计、实现、集成测试、确认测试、使用维护,并且规定,单元测试是在实现阶段完成的,集成测试的计划应当在概要设计阶段制定的,确认测试的计划应当在需求分析阶段制定的。 【例4】集成测试也叫做( A )或( B )。通常,在( C )的基础上,将所有模块按照设计要求组装成为系统。子系统的集成测试特别称为( D ),它所做的工作是要找出子系统和系统需求规格说明之间的( E )。需要考虑的问题是:在把各个模块连接起来的时侯,穿越模块接口的数据是否会( F ); 一个模块的功能是否会对另一个模块的功能产生不利的影响;各个( G )组合起来,能否达到预期要求的( H );( I )是否有问题;单个模块的误差累积起来是否会放大。 供选择的答案: A ( D. ① 单元测试 ② 部件测试 ③ 组装测试 ④ 系统测试 ⑤ 确认测试 ⑥ 联合测试 E ( I. ① 子功能 ② 丢失 ③ 父功能 ④ 局部数据结构 ⑤ 全局数据结构 ⑥ 不一致 ⑦ 一致 答案:A. ③ B. ⑥ C. ① D. ② E. ⑥ F. ② G. ① H. ③ I. ⑤ A、B的答案可互换 分析:集成测试也叫做组装测试或联合测试。通常,在单元测试的基础上,将所有模块按照设计要求组装成为系统。子系统的集成测试特别称为部件测试,它所做的工作是要找出子系统和系统需求规格说明之间的不一致。这时需要考虑的问题是: (1) 在把各个模块连接起来的时侯,穿越模块接口的数据是否会丢失; (2) 一个模块的功能是否会对另一个模块的功能产生不利的影响; (3) 各个子功能组合起来,能否达到预期要求的父功能; (4) 全局数据结构是否有问题; (5) 单个模块的误差累积起来是否会放大。 【例5】如图所示的程序有四条不同的路径。分别表示为L1(a→c→e)、L2(a→b→d)、L3(a→b→e) 和L4(a→c→d),或简写为ace、abd、abe及acd。由于覆盖测试的目标不同,逻辑覆盖方法可以分为语句覆盖、判定覆盖、条件覆盖、判定–条件覆盖、条件组合覆盖和路径覆盖。 从备选的答案中选择适当的测试用例与之匹配。( A )属于语句覆盖;( B )、( C )属于判定覆盖;( D )、( E )属于条件覆盖;( F )、( G )属于判定–条件覆盖;( H )属于条件组合覆盖;( I )属于路径覆盖。 供选择的答案: A. ( I. ① 【(2,0,4),(2,0,3)】覆盖 ace; 【(1,1,1),(1,1,1)】覆盖 abd; 判断条件覆盖 ② 【(1,0,3),(1,0,4)】覆盖abe; 【(2,1,1),(2,1,2)】覆盖 abe;条件覆盖 ③ 【(2,0,4),(2,0,3)】覆盖 ace; ④ 【(2,1,1),(2,1,2)】覆盖 abe; 【(3,0,3),(3,1,1)】覆盖 acd; ⑤ 【(2,0,4),(2,0,3)】覆盖 ace; 【(1,0,1),(1,0,1)】覆盖 abd; 【(2,1,1),(2,1,2)】覆盖 abe; 条件覆盖, 判条 ⑥ 【(2,0,4),(2,0,3)】覆盖 ace 【(1,1,1),(1,1,1)】覆盖 abd;路径覆盖 【(1,1,2),(1,1,3)】覆盖 abe; 【(3,0,3),(3,0,1)】覆盖acd; ⑦ 【(2,0,4),(2,0,3)】覆盖 ace; 【(1,1,1),(1,1,1)】覆盖 abd; 条件组合 【(1,0,3),(1,0,4)】覆盖 abe; 【(2,1,1),(2,1,2)】覆盖 abe; 答案:A. ③ B. ① C. ④ D. ② E. ⑤ F. ① G. ⑤ H. ⑦ I. ⑥ B、C的答案可互换。D、E的答案可互换。F、G的答案可互换。 分析:(1) 语句覆盖 所谓语句覆盖就是设计若干个测试用例,运行被测程序,使得每一可执行语句至少执行一次。例如在本题所给出的例子中,正好所有的可执行语句都在路径L1上, 所以选择路径L1设计测试用例,就可以覆盖所有的可执行语句。 测试用例的设计格式如下 【输入的(A, B, x),输出的(A, B, x)】 本题满足语句覆盖的测试用例是: 【(2, 0, 4),(2, 0, 3)】 覆盖 ace【L1】 (答案③) 从程序中每个可执行语句都得到执行这一点来看,语句覆盖的方法似乎能够比较全面地检验每一个可执行语句。但需要注意的是,这种覆盖也绝不是完美无缺的。假设图中两个判断的逻辑运算有问题,例如,第一个判断中的逻辑运算符“”错写成了“”, 或者第二个判断中的逻辑运算符“”错写成了“”,利用上面的测试用例,仍可覆盖所有4个可执行语句。这说明虽然做到了语句覆盖,但可能发现不了判断中逻辑运算中出现的错误。与后面所介绍的其它覆盖相比,语句覆盖是最弱的逻辑覆盖准则。 (2) 判定覆盖 所谓判定覆盖就是设计若干个测试用例,运行被测程序,使得程序中每个判断的取真分支和取假分支至少经历一次。判定覆盖又称为分支覆盖。对于本题所给出的例子,如果选择路径L1和L2,就可得满足要求的测试用例: 【(2, 0, 4),(2, 0, 3)】 覆盖 ace【L1】 (答案①) 【(1, 1, 1),(1, 1, 1)】 覆盖 abd【L2】 如果选择路径L3和L4,还可得另一组可用的测试用例: 【(2, 1, 1),(2, 1, 2)】 覆盖 abe【L3】 (答案④) 【(3, 0, 3),(3, 1, 1)】 覆盖 acd【L4】 所以,测试用例的取法不唯一。注意有例外情形,例如,若把图例中第二个判断中的条件x>1错写成x<1,那么利用上面两组测试用例,仍能得到同样结果。这表明,只是判定覆盖,还不能保证一定能查出在判断的条件中存在的错误。因此,还需要更强的逻辑覆盖准则去检验判断内部条件。 (3) 条件覆盖 所谓条件覆盖就是设计若干个测试用例,运行被测程序,使得程序中每个判断的每个条件的可能取值至少执行一次。例如在本题所给出的例子中,我们事先可对所有条件的取值加以标记。例如, 对于第一个判断:条件 A>1 取真值为,取假值为 条件 B=0 取真值为,取假值为 对于第二个判断:条件 A=2 取真值为,取假值为 条件 x>1 取真值为,取假值为 则可选取测试用例如下: 测试用例(答案⑤)  通过路径  条件取值  覆盖分支   【(2, 0, 4),(2, 0, 3)】 【(1, 0, 1),(1, 0, 1)】 【(2, 1, 1),(2, 1, 2)】  ace (L1) abd (L2) abe (L3)               c,e b,d b,e  或 测试用例(答案②)  通过路径  条件取值  覆盖分支   【(1, 0, 3),(1, 0, 4)】  abe (L3)       b,e   【(2, 1, 1),(2, 1, 2)】  abe (L3)       b,e  注意,前一组测试用例不但覆盖了所有判断的取真分支和取假分支,而且覆盖了判断中所有条件的可能取值。但是后一组测试用例虽满足了条件覆盖,但只覆盖了第一个判断的取假分支和第二个判断的取真分支,不满足判定覆盖的要求。为解决这一矛盾,需要对条件和分支兼顾,有必要考虑以下的判定-条件覆盖。 (4) 判定-条件覆盖 所谓判定-条件覆盖就是设计足够的测试用例,使得判断中每个条件的所有可能取值至少执行一次,同时每个判断本身的所有可能判断结果至少执行一次。换言之,即是要求各个判断的所有可能的条件取值组合至少执行一次。例如,对于本题所给例子中的各判断,若、、、及、、、 的含意如前所述,则只需设计以下两个测试用例便可覆盖图中的8个条件取值以及4个判断分支。 测试用例(答案①)  通过路径  条件取值  覆盖分支   【(2, 0, 4),(2, 0, 3)】  ace (L1)       c,e   【(1, 1, 1),(1, 1, 1)】  abd (L2)       b,d  判定-条件覆盖也有缺陷。从表面上来看,它测试了所有条件的取值。但是事实并非如此。因为往往某些条件掩盖了另一些条件。对于条件表达式(A>1)and(B=0)来说,若(A>1)的测试结果为真,则还要测试(B=0),才能决定表达式的值;而若(A>1)的测试结果为假,可以立刻确定表达式的结果为假。这时,往往就不再测试(B=0)的取值了。因此,条件(B=0)就没有检查。同样,对于条件表达式(A=2)or(X>1)来说,若(A=2)的测试结果为真,就可以立即确定表达式的结果为真。这时,条件(X>1)就没有检查。因此,采用判定-条件覆盖,逻辑表达式中的错误不一定能够查得出来。 (5) 条件组合覆盖 所谓条件组合覆盖就是设计足够的测试用例,运行被测程序,使得每个判断的所有可能的条件取值组合至少执行一次。现在我们仍来考察本题所给出的例子,先对各个判断的条件取值组合加以标记。例如, 记 ① A>1,B=0 作 ,属第一个判断的取真分支; ② A>1,B≠0 作 ,属第一个判断的取假分支; ③ A≯1,B=0 作 ,属第一个判断的取假分支; ④ A≯1,B≠0 作 ,属第一个判断的取假分支; ⑤ A=2,x>1 作 ,属第二个判断的取真分支; ⑥ A=2,x≯1 作 ,属第二个判断的取真分支; ⑦ A≠2,x>1 作 ,属第二个判断的取真分支; ⑧ A≠2,x≯1 作 ,属第二个判断的取假分支; 对于每个判断,要求所有可能的条件取值的组合都必须取到。在图中的每个判断各有两个条件,所以各有4个条件取值的组合。我们取4个测试用例,就可用以覆盖上面8种条件取值的组合。必须明确,这里并未要求第一个判断的4个组合与第二个判断的4个组合再进行组合。要是那样的话,就需要42=16个测试用例了。 测试用例(答案⑦)  通过路径  覆盖条件  覆盖组合号   【(2, 0, 4),(2, 0, 3)】  ace (L1)       ①,⑤   【(2, 1, 1),(2, 1, 2)】  abe (L3)       ②,⑥   【(1, 0, 3),(1, 0, 4)】  abe (L3)       ③,⑦   【(1, 1, 1),(1, 1, 1)】  abd (L2)       ④,⑧  这组测试用例覆盖了所有条件的可能取值的组合,覆盖了所有判断的可取分支,但路径漏掉了L4。测试还不完全。 (6) 路径测试 路径测试就是设计足够的测试用例,覆盖程序中所有可能的路径。若还是看本题所给出的例子,则可以选择如下的一组测试用例来覆盖该程序段的全部路径。 测试用例(答案⑥)  通过路径  覆盖条件   【(2, 0, 4),(2, 0, 3)】  ace (L1)        【(1, 1, 1),(1, 1, 1)】  abd (L2)        【(1, 1, 2),(1, 1, 3)】  abe (L3)        【(3, 0, 3),(3, 0, 1)】  acd (L4)       【例6】下面是快速排序算法中的一趟划分算法,其中datalist是数据表,它有两个数据成员:一是元素类型为Element的数组V,另一个是数组大小n。算法中用到两个操作,一是取某数组元素V[i]的关键码操作getKey ( ),一是交换两数组元素内容的操作Swap( ): int Partition ( datalist &list, int low, int high ) { //在区间[ low, high ]以第一个对象为基准进行一次划分,k返回基准对象回放位置。 int k = low; Element pivot = list.V[low]; //基准对象 for ( int i = low+1; i <= high; i++ ) //检测整个序列,进行划分 if ( list.V[i].getKey ( ) < pivot.getKey( ) && ++ k != i ) Swap ( list.V[k], list.V[i] ); //小于基准的交换到左侧去 Swap ( list.V[low], list.V[k] ); //将基准对象就位 return k; //返回基准对象位置 } (1) 试画出它的程序流程图; (2) 试利用路径覆盖方法为它设计足够的测试用例(循环次数限定为0次,1次和2次)。 答案:(1)流程图如下。 (2) 测试用例设计 循环 次数  输 入 条 件  输 出 结 果  执 行 路 径   low high k i V[0] V[1] V[2] k i V[0] V[1] V[2]    0  0  0 0 1  (  (  ( 0 1  (  (  ( ①③   1  0  1 0 1  1  2  ( 0 2  1  2  ( ①②⑤⑥③    0  1 0 1  2  1  ( 1 2  1  2  ( ①②④⑥③    0  1 0 1  1  1  ( 0 2  1  1  ( ①②⑤⑥③   2  0  2 0 1  1  2  3 0 3  1  2  3 ①②⑤⑥②⑤⑥③    0  2 0 1  1  2  1 0 3  1  2  1 ①②⑤⑥②⑤⑥③    0  2 0 1  2  3  1 1 3  1  2  3 ①②⑤⑥②④⑥③    0  2 0 1  3  2  1 2 3  1  2  3 ①②④⑥②④⑥③    0  2 0 1  2  1  2 1 3  1  2  2 ①②④⑥②⑤⑥③    0  2 0 1  2  1  3 1 3  1  2  3 ①②④⑥②⑤⑥③    0  2 0 1  1  1  2 0 3  1  1  2 ①②⑤⑥②⑤⑥③    0  2 0 1  2  2  1 1 3  1  2  2 ①②⑤⑥②④⑥③    0  2 0 1  2  2  2 0 3  2  2  2 ①②⑤⑥②⑤⑥③   分析:画程序流程图是设计测试用例的关键。从以往同学解题的经验来看,在画流程图时就出错了。所以首先要把流程图中的逻辑关系搞清楚再画出正确的流程图。考虑测试用例设计需要首先有测试输入数据,还要有预期的输出结果。对于此例,控制循环次数靠循环控制变量i和循环终值high。循环0次时,取low = high,此时一次循环也不做。循环一次时,取low +1 = high,循环二次时,取low+2 = high。 根据BRO策略,条件V[i] < pivot && ++k≠i的约束集合为 { (<, < ), (<, = ), (=, <), (>, < ) },因此,测试用例设计为: 循环 次数  输 入 条 件   输 出 结 果    low high k i V[0] V[1] V[2] pivot k i V[0] V[1] V[2]    0  0  0 0 1  (  (  (  ( 0 1  (  (  (    1  0  1 0 1  1  2   1 0 2  1  2  (>, <)    0  1 0 1  2  1   2 1 2  2 1  1 2  (<, =)    0  1 0 1  1  1   1 0 2  1  1  (=, <)    0  1 0 1  不  可  达       (<, <)   2  0  2 0 1  1  2  3  1 0 0 2 3  1 1  2 2  3 3 (>, <) (>, <)    0  2 0 1  1  2  1  1 0 0 2 3  1 1  2 2  1 1 (>, <) (=, <)    0  2 0 1  2  3  1  2 0 1 2 3  2 2 1  3 1 2  1 3 3 (>, <) (<, <)    0  2 0 1  不  可  达       (>, <) (<, =)    0  2 0 1  3  2  1  3 1 2 2 3  3 3 1  2 2 2  1 1 3 (<, =) (<, =)    0  2 0 1  2  1  2  2 1 1 2 3  2 2 1  1 1 2  2 2 2 (<, =) (=, <)    0  2 0 1  2  1  3  2 1 1 2 3  2 2 1  1 1 2  3 3 3 (<, =) (>, <)    0  2 0 1  不  可  达       (<, =) (<, <)    0  2 0 1  1  1  2  1 0 0 2 3  1 1  1 1  2 2 (=, <) (>, <)    0  2 0 1  2  2  1  2 0 1 2 3  2 2 1  2 1 2  1 2 2 (=, <) (<, <)    0  2 0 1  2  2  2  2 0 0 2 3  2 2  2 2  2 2 (=, <) (=, <)    0  2 0 1  不  可  达       (=, <) (<, =)    0  2 0 1  不  可  达       (<, <) (*, *)   四、习题 【5-1】从供选择的答案中选出应填入下列( )中的字句。 软件测试的目的是( A )。为了提高测试的效率,应该( B )。使用白盒测试方法时,确定测试数据应根据( C )和指定的覆盖标准。与设计测试数据无关的文档是( D )。 软件的集成测试工作最好由( E )承担,以提高集成测试的效果。 供选择的答案: A. ① 评价软件的质量 ② 发现软件的错误 ③ 找出软件中的所有错误 ④ 证明软件是正确的 B. ① 随机地选取测试数据 ② 取一切可能的输入数据作为测试数据 ③ 在完成编码以后制定软件的测试计划 ④ 选择发现错误的可能性大的数据作为测试数据 C. ① 程序的内部逻辑 ② 程序的复杂程度 ③ 使用说明书 ④ 程序的功能 D. ① 该软件的设计人员 ② 程序的复杂程度 ③ 源程序 ④ 项目开发计划 E. ① 该软件的设计人员 ② 该软件开发组的负责人 ③ 该软件的编程人员 ④ 不属于该软件开发组的软件设计人员 【5-2】请从供选择的答案中选出应填入下列( )中的字句。 程序的三种基本控制结构是( A )。它们的共同点是( B )。结构化程序设计的一种基本方法是( C )。软件测试的目的是( D )。软件调试的目的是( E )。 供选择的答案: A. ① 过程,子程序,分程序 ② 顺序,条件,循环 ③ 递归,堆栈,队列 ④ 调用,返回,转移 B. ① 不能嵌套使用 ② 只能用来写简单的程序 ③ 已经用硬件实现 ④ 只有一个入口和一个出口 C. ① 筛选法 ② 递归法 ③ 归纳法 ④ 逐步求精法 D. ① 证明程序中没有错误 ② 发现程序中的错误 ③ 测量程序的动态特性 ④ 检查程序中的语法错误 E. ① 找出错误所在并改正之 ② 排除存在错误的可能性 ③ 对错误性质进行分类 ④ 统计出错的次数 【5-3】从下列关于软件测试的叙述中,选出5条正确的叙述。 (1) 用黑盒法测试时,测试用例是根据程序内部逻辑设计的。 (2) 尽量用公共过程或子程序去代替重复的代码段。 (3) 测试是为了验证该软件已正确地实现了用户的要求。 (4) 对于连锁型分支结构,若有n个判定语句,则有2n条路径。 (5) 尽量采用复合的条件测试,以避免嵌套的分支结构。 (6) GOTO语句概念简单,使用方便,在某些情况下,保留GOTO语句反能使写出的程序更加简洁。 (7) 发现错误多的程序模块,残留在模块中的错误也多。 (8) 黑盒测试方法中最有效的是因果图法。 (9) 在做程序的单元测试时,桩(存根)模块比驱动模块容易编写。 (10) 程序效率的提高主要应通过选择高效的算法来实现。 【5-4】从供选择的答案中选出同下列关于软件测试的各条叙述关系最密切的字句。 (1) 对可靠性要求很高的软件,例如操作系统,由第三者对源代码进行逐行检查。 (2) 已有的软件被改版时,由于受到变更的影响,改版前正常的功能可能发生异常,性能也可能下降。因此,对变更的软件进行测试是必要的。 (3) 在意识到被测试模块的内部结构或算法的情况下进行测试。 (4) 为了确认用户的需求,先做出系统的主要部分,提交给用户试用。 (5) 在测试具有层次结构的大型软件时,有一种方法是从上层模块开始,由上到下进行测试。此时,有必要用一些模块替代尚未测试过的下层模块。 供选择的答案: A ( E: ① 仿真器 ② 代码审查 ③ 模拟器 ④ 桩 ⑤ 驱动器 ⑥ 域测试 ⑦ 黑盒测试 ⑧ 原型 ⑨ 白盒测试 ⑩ 退化测试 【5-5】对小的程序进行穷举测试是可能的,用穷举测试能否保证程序是百分之百正确呢? 【5-6】在任何情况下单元测试都是可能的吗?都是需要的吗? 【5-7】从供选择的答案中选出应填入下面有关软件测试的叙述的( )内的正确答案。 软件测试方法可分为黑盒测试法和白盒测试法两种。 黑盒测试法是通过分析程序的( A )来设计测试用例的方法。除了测试程序外,它还适用于对( B )阶段的软件文档进行测试。 白盒测试法是根据程序的( C )来设计测试用例的方法。除了测试程序外,它也适用于对( D )阶段的软件文档进行测试。 白盒法测试程序时常按照给定的覆盖条件选取测试用例。( E )覆盖比( F )覆盖严格,它使得每一个判定的每一条分支至少经历一次。( G )覆盖既是判定覆盖,又是条件覆盖,但它并不保证使各种条件都能取到所有可能的值。( H )覆盖比其他条件都要严格,但它不能保证覆盖程序中的每一条路径。 单元测试一般以( I )为主,测试的依据是( J )。 供选择的答案: A, C:① 应用范围 ② 内部逻辑 ③ 功能 ④ 输入数据 B, D:① 编码 ② 软件详细设计 ③ 软件总体设计 ④ 需求分析 E, F, G, H:① 语句 ② 判定 ③ 条件 ④ 判定/条件 ⑤ 多重条件 ⑥ 路径 I:① 白盒法 ② 黑盒法 J:① 模块功能规格说明 ② 系统模块结构图 ③ 系统需求规格说明 【5-8】从供选择的答案中选出应该填入下列关于软件测试的叙述的( )内的正确答案。 软件测试中常用的静态分析方法是( A )和( B )。( B )用于检查模块或子程序间的调用是否正确。分析方法(白盒方法)中常用的方法是( C )方法。非分析方法(黑盒方法)中常用的方法是( D )方法和( E )方法。( E )方法根据输出对输入的依赖关系设计测试用例。 供选择的答案: A ( B: ① 引用分析 ② 算法分析 ③ 可靠性分析 ④ 效率分析 ⑤ 接口分析 ⑥ 操作分析 C ~ E: ① 路径测试 ② 等价类 ③ 因果图 ④ 归纳测试 ⑤ 综合测试 ⑥ 追踪 ⑦ 深度优先 ⑧ 调试 ⑨ 相对图 【5-9】下面是选择排序的程序,其中datalist是数据表,它有两个数据成员:一是元素类型为Element的数组V,另一个是数组大小n。算法中用到两个操作,一是取某数组元素V[i]的关键码操作getKey ( ),一是交换两数组元素内容的操作Swap( ):: void SelectSort ( datalist & list ) { //对表list.V[0]到list.V[n-1]进行排序, n是表当前长度。 for ( int i = 0; i < list.n-1; i++ ) { int k = i; //在list.V[i].key到list.V[n-1].key中找具有最小关键码的对象 for ( int j = i+1; j < list.n; j++) if ( list.V[j].getKey ( ) < list.V[k].getKey ( ) ) k = j; //当前具最小关键码的对象 if ( k != i ) Swap ( list.V[i], list.V[k] ); //交换 } } (1) 试计算此程序段的McCabe复杂性; (2) 用基本路径覆盖法给出测试路径; (3) 为各测试路径设计测试用例。 【5-10】根据下面给出的规格说明,利用等价类划分的方法,给出足够的测试用例。 “一个程序读入三个整数。把此三个数值看成是一个三角形的三个边。这个程序要打印出信息,说明这个三角形是三边不等的、是等腰的、还是等边的。” 【5-11】设要对一个自动饮料售货机软件进行黑盒测试。该软件的规格说明如下: “有一个处理单价为1元5角钱的盒装饮料的自动售货机软件。若投入1元5角硬币,按下“可乐”、“雪碧”或“红茶”按钮,相应的饮料就送出来。若投入的是2元硬币,在送出饮料的同时退还5角硬币。” (1) 试利用因果图法,建立该软件的因果图; (2) 设计测试该软件的全部测试用例。 【5-12】对一个长度为100,000条指令的程序进行测试,记录下来的数据如下: ( 测试开始, 发现错误个数为0; ( 经过160小时的测试, 累计改正100个错误, 此时, MTTF = 0.4小时; ( 又经过160小时的测试, 累计改正300个错误, 此时, MTTF = 2小时; (1) 估计程序中固有的错误总数; (2) 为使MTTF达到10小时, 必须测试和调试这个程序多长时间? (3) 给出MTTF与测试时间t之间的函数关系。 【5-13】应该由谁来进行确认测试?是软件开发者还是软件用户?为什么? 五、习题解答 【5-1】A. ② B. ④ C. ① D. ④ E. ④ 软件测试的目的是软件中的错误。因为不可能把所有可能的输入数据都拿来测试(时间花费不起),为了提高测试的效率,应该选择发现错误的可能性大的数据作为测试数据。 使用白盒测试方法时,确定测试数据应根据程序的内部逻辑和指定的覆盖标准,可以不考虑程序的功能。与设计测试数据无关的文档是项目开发计划。 软件的集成测试工作最好由不属于该软件开发组的软件设计人员承担,以提高集成测试的效果。 【5-2】A. ② B. ④ C. ④ D. ② E. ① 1966年,Bohm与Jacopini提出任何单入口单出口的没有“死循环”的程序都能由三种最基本的控制结构构造出来。这三种基本控制结构就是“顺序结构”、“选择IF-THEN-ELSE结构”、“重复DO-WHILE或DO-UNTIL结构”。 它们的共同点是只有一个入口和一个出口。E.W.Dijkstra提出了程序要实现结构化的主张,并将这一类程序设计称为结构化程序设计。这种方法的一个重要原则就是采用自顶向下、逐步求精的方法编写程序。N.Wirth曾做过如下说明:“我们对付一个复杂问题的最重要的方法就是抽象。 因此,对于一个复杂的问题,不要急于马上用计算机指令、数字和逻辑符号来表示它,而应当先用较自然的抽象的语句来表示,从而得到抽象的程序。抽象程序对抽象的数据类型进行某些特定的运算,并用一些合适的记号(可以是自然语言)来表示。下一步对抽象程序再做分解,进入下一个抽象的层次。这样的细化过程一直进行下去,直到程序能被计算机接受为止。此时的程序已经是用某种高级语言或机器指令书写的了。” 软件调试则是在进行了成功的测试之后才开始的工作。它与软件测试不同,软件测试的目的是尽可能多地发现软件中的错误,但进一步诊断和改正程序中潜在的错误,则是调试的任务。调试活动由两部分组成:① 确定程序中可疑错误的确切性质和位置。② 对程序(设计,编码)进行修改,排除这个错误。 【5-3】正确的叙述有(4)、(5)、(6)、(7)、(10)。 黑盒测试主要是根据程序的有关功能规格说明和覆盖准则来设计测试用例,进行测试的,不是根据程序的内部逻辑来设计测试用例,这是白盒测试做的事情。在所有黑盒测试方法中,最有效的不是因果图法,而是边界值分析方法。测试的目的是尽可能多地发现软件中的错误,其附带的收获才是验证该软件已正确地实现了用户的要求。测试的一条重要原则是:发现错误多的程序模块,残留在模块中的错误也多。软件可靠性模型(Shooman)就是依据这个原则建立它的公式的。对于连锁型分支结构,若有n个判定语句,则有2n条路径。因此,随着n的增大,路径数增长非常快。单元测试时,因为桩模块要模拟子模块的功能,这不是一件容易的事情,而驱动模块只是控制被测模块的执行,所以桩模块的编写比驱动模块的编写要难得多。 在程序设计风格方面,如果重复的代码段没有明显的功能,不可以抽取出来形成独立的公共过程或子程序,只有在这些代码段表现出独立的功能时,才可把它们抽取出来形成独立的公共过程或子程序。另外,程序效率的提高主要应通过选择高效的算法或使用高效的语言编译器来实现。GOTO语句概念简单,使用方便,在某些情况下,保留GOTO语句反能使写出的程序更加简洁,这句话是正确的。 【5-4】(1) ② (2) ⑩ (3) ⑨ (4) ⑧ (5) ④ (1) 对可靠性要求很高的软件,由第三者对源代码进行逐行检查,这是代码审查。 (2) 软件变更时可能发生退化现象:原来正常的功能可能发生异常,性能也可能下降。因此,对变更的软件要进行退化测试。 (3) 基于被测试模块的内部结构或算法设计测试用例进行测试,这是白盒测试。 (4) 为了确认用户的需求,先做出系统的原型,提交给用户试用。 (5) 自顶向下对具有层次结构的大型软件进行集成测试时,需要设计一些虚拟模块来替代尚未测试过的下层模块,这些模块叫做桩模块。 【5-5】对小程序进行穷举测试,不见得能保证程序百分之百正确。所谓穷举测试是拿所有可能的输入数据来作为测试用例(黑盒测试),或覆盖程序中所有可能的路径(白盒测试)。对于小程序来说,实际上并不能真正作到穷举测试。例如前面讲过,一个小程序P只有两个输入X和Y及输出Z,在字长为32位的计算机上运行。如果X、Y只取整数,考虑把所有的X、Y值都做为测试数据,按黑盒方法进行穷举测试,这样做可能采用的测试数据组(Xi,Yi),基数(radix)i的最大可能数目为:232×232=264。如果程序P测试一组X、Y数据需要1毫秒,而且假定一天工作24小时,一年工作365天,要完成264组测试,需要5亿年。 【5-6】单元测试又称模块测试,是针对软件设计的最小单位─程序模块,进行正确性检验的测试工作。其目的在于发现各模块内部可能存在的各种差错。单元测试需要从程序的内部结构出发设计测试用例。多个模块可以平行地独立进行单元测试。 单元测试是在编码阶段完成的,每编写出一个程序模块,就开始做这个模块的单元测试,所以只要采用模块化方法开发软件,单元测试都是必需的。它可由编写程序的人来完成。因为它需要根据程序的内部结构设计测试用例,对于那些不了解程序内部细节的人,这种测试无法进行。 【5-7】A. ③ B. ④ C. ② D. ② E. ② F. ① G. ④ H. ⑤ I. ① J. ① 软件测试方法可分为黑盒测试法和白盒测试法两种。黑盒测试法是基于程序的功能来设计测试用例的方法。除了测试程序外,它还适用于对需求分析阶段的软件文档进行测试。白盒测试法是根据程序的内部逻辑来设计测试用例的方法。除了测试程序外,它也适用于对软件详细设计阶段的软件文档进行测试。 白盒法测试程序时常按照给定的覆盖条件选取测试用例。判定覆盖比语句覆盖严格,它使得每一个判定的每一条分支至少经历一次。判定/条件覆盖既是判定覆盖,又是条件覆盖,但它并不保证使各种条件都能取到所有可能的值。多重条件覆盖,也叫组合条件覆盖,比其他条件都要严格,但它不能保证覆盖程序中的每一条路径。 单元测试一般以白盒法为主,测试的依据是系统的模块功能规格说明。 【5-8】A. ① B. ⑤ C. ① D. ② E. ③ 软件测试中常用的静态分析方法是引用分析和接口分析。接口分析用于检查模块或子程序间的调用是否正确。分析方法(白盒方法)中常用的方法是路径测试方法。非分析方法(黑盒方法)中常用的方法是等价类(划分)方法和因果图方法。因果图方法根据输出对输入的依赖关系设计测试用例。 【5-9】(1) McCabe环路复杂性 = 5 (2) 独立路径有5条: ①③ ①②⑤⑧…… ①②⑤⑨…… ①②④⑥…… ①②④⑦…… (3) 为各测试路径设计测试用例: 路径①③:取n = 1 路径①②⑤⑧……:取n = 2, 预期结果:路径⑤⑧③不可达 路径①②⑤⑨……:取n = 2, 预期结果:路径⑤⑨③不可达 路径①②④⑥⑤⑧③: 取n = 2, V[0] = 2, V[1] = 1, 预期结果:k = 1, V[0] = 1, V[1] = 2 路径①②④⑥⑤⑨③: 取n = 2, V[0] = 2, V[1] = 1, 预期结果:k = 1, 路径⑨③不可达 路径①②④⑦⑤⑧③: 取n = 2, V[0] = 1, V[1] = 2, 预期结果:k = 0, 路径⑧③不可达 路径①②④⑦⑤⑨③: 取n = 2, V[0] = 1, V[1] = 2, 预期结果:k = 0, V[0] = 1, V[1] = 2 【5-10】设三角形的三条边分别为A, B, C。如果它们能够构成三角形的三条边,必需满足: A > 0,B > 0,C > 0,且A + B > C,B + C > A,A + C > B。 如果是等腰的,还要判断是否A = B,或B = C,或A = C。 对于等边的,则需判断是否A = B,且B = C,且A = C。 列出等价类表: 输入条件  有效等价类  无效等价类  是否三角形的三条边 (A > 0) (1), (B > 0) (2), (C > 0) (3), (A + B > C), (4) (B + C > A) (5), (A + C > B) (6) A ( 0 (7), B ( 0 (8), C ( 0 (9), A + B ( C (10), A + C ( B (11), B + C ( A (12)  是否等腰三角形 (A = B) (13), (B = C) (14), (A = C) (15) (A ( B) and (B ( C) and (A ( C) (16)  是否等边三角形 (A = B) and (B = C) and (A = C) (17) (A ( B) (18), (B ( C) (19), (A ( C) (20)   设计测试用例:输入顺序是〖A,B,C〗 (〖3,4,5〗覆盖等价类 (1), (2), (3), (4), (5), (6)。满足即为一般三角形。 (〖0,1,2〗覆盖等价类 (7)。不能构成三角形。 若不考虑特定A, B, C, (〖1,0,2〗覆盖等价类 (8)。同上。 三者取一即可 (〖1,2,0〗覆盖等价类 (9)。同上。 (〖1,2,3〗覆盖等价类 (10)。同上。 若不考虑特定A, B, C, (〖1,3,2〗覆盖等价类 (11)。同上。 三者取一即可 (〖3,1,2〗覆盖等价类 (12)。同上。 (〖3,3,4〗覆盖等价类 (1), (2), (3), (4), (5), (6), (13)。 满足即为等腰三角形, (〖3,4,4〗覆盖等价类 (1), (2), (3), (4), (5), (6), (14)。 若不考虑特定A, B, C, (〖3,4,3〗覆盖等价类 (1), (2), (3), (4), (5), (6), (15)。 三者取一即可 (〖3,4,5〗覆盖等价类 (1), (2), (3), (4), (5), (6), (16)。不是等腰三角形。 (〖3,3,3〗覆盖等价类 (1), (2), (3), (4), (5), (6), (17)。是等边三角形 (〖3,4,4〗覆盖等价类 (1), (2), (3), (4), (5), (6), (14), (18)。 不是等边三角形, (〖3,4,3〗覆盖等价类 (1), (2), (3), (4), (5), (6), (15), (19)。 若不考虑特定A, B, C, (〖3,3,4〗覆盖等价类 (1), (2), (3), (4), (5), (6), (13), (20)。 三者取一即可 【5-11】(1) 因果图 输入条件(原因) 输出条件(结果) 投入1元5角硬币 (1) (21) 退还5角硬币 E 投入2元硬币 (2) (11) (22) 送出“可乐”饮料 按“可乐”按钮 (3) (23) 送出“雪碧”饮料 按“雪碧”按钮 E (4) ( (12) (24) 送出“红茶”饮料 按“红茶”按钮 (5) (2) 测试用例设计 1 2 3 4 5 6 7 8 9 10 11   输 入 投入1元5角硬币 (1) 1 1 1 1 0 0 0 0 0 0 0   投入2元硬币 (2) 0 0 0 0 1 1 1 1 0 0 0   按“可乐”按钮 (3) 1 0 0 0 1 0 0 0 1 0 0   按“雪碧”按钮 (4) 0 1 0 0 0 1 0 0 0 1 0   按“红茶”按钮 (5) 0 0 1 0 0 0 1 0 0 0 1  中间 结点  已投币 (11) 1 1 1 1 1 1 1 1 0 0 0    已按钮 (12) 1 1 1 0 1 1 1 0 1 1 1   输 出 退还5角硬币 (21) 0 0 0 0 1 1 1 0 0 0 0   送出“可乐”饮料 (22) 1 0 0 0 1 0 0 0 0 0 0   送出“雪碧”饮料 (23) 0 1 0 0 0 1 0 0 0 0 0   送出“红茶”饮料 (24) 0 0 1 0 0 0 1 0 0 0 0  测试用例 每一纵列为一个测试用例 【5-12】由shooman公式: 其中,IT = 105,MTTF1 = 0.4,T1 = 160,n1 = 100,MTTF2 = 2,T2 = 320,n2 = 300。得: (1) 解得程序中固有得错误总数ET = 350,此外K = 1000。 (2) 设MTTF = 10,有 解得x = 340。由可靠性累积曲线EC (t) = ET (1-e-K1 t ), 得 100 = 350 (1-e-160K1) 300 = 350 (1-e-320K1) 解得 K1 ( 0.01。代入:340 = 350 (1-e-K1 t ) = 350(1-e-0.01 t ) t = (ln(35))/0.01 ( 356 (小时) 因此求得为使MTTF = 10,测试和调试该程序需要花费356小时。 (3) MTTF与测试时间t之间的函数关系: 因为EC (t) = ET (1-e-K1 t ),则ET - EC (t) = ET e-K1 t。代入shooman公式: 【5-13】在对照需求做有效性测试和软件配置审查时,是由软件开发者在开发环境下进行的测试。而接下来做验收测试时则以用户为主。软件开发人员和QA(质量保证)人员也应参加。由用户参加设计测试用例,使用用户界面输入测试数据,并分析测试的输出结果。一般使用生产中的实际数据进行测试。 如果软件是为多个客户开发的,则需要进行α测试和β测试。α测试是由一个用户在开发环境下进行的测试,也可以是公司内部的用户在模拟实际操作环境下进行的测试。软件在一个自然设置状态下使用。开发者坐在用户旁边,随时记下错误情况和使用中的问题。这是在受控制的环境下进行的测试。 β测试是由软件的多个用户在一个或多个用户的实际使用环境下进行的测试。这些用户是与公司签定了支持产品预发行合同的外部客户,他们要求使用该产品,并愿意返回有关错位错误信息给开发者。与α测试不同的是,开发者通常不在测试现场。因而,β测试是在开发者无法控制的环境下进行的软件现场应用。