900 likes | 1.03k Views
第七章 实现. 通常把编码和测试统称为实现。 编码 就是把软件设计结果翻译成用某种程序设计语言书写的程序。 测试 的目的就是在软件投入生产性运行之前,尽可能多地发现软件中的错误。. 软件测试在软件生命周期中横跨两个阶段。 单元测试: 通常在编写出每个模块之后就对它做必要的测试 ( 称为单元测试 ) ,模块的编写者和测试者是同一个人,编码和单元测试属于软件生命周期的同一个阶段。 综合测试: 在这个阶段结束之后,对软件系统还应该进行各种综合测试,这是软件生命周期中的另一个独立的阶段,通常由专门的测试人员承担这项工作。
E N D
第七章 实现 通常把编码和测试统称为实现。 编码就是把软件设计结果翻译成用某种程序设计语言书写的程序。 测试的目的就是在软件投入生产性运行之前,尽可能多地发现软件中的错误。
软件测试在软件生命周期中横跨两个阶段。 单元测试:通常在编写出每个模块之后就对它做必要的测试(称为单元测试),模块的编写者和测试者是同一个人,编码和单元测试属于软件生命周期的同一个阶段。 综合测试:在这个阶段结束之后,对软件系统还应该进行各种综合测试,这是软件生命周期中的另一个独立的阶段,通常由专门的测试人员承担这项工作。 大量统计资料表明,软件测试的工作量往往占软件开发总工作量的40%以上。
7.1 编码 7.1.1 程序设计语言
1 程序设计语言的分类 大体上,程序设计语言分为以下几类: (1)机器语言 (2)汇编语言 (3)高级语言 由于高级语言种类繁多,我们可以从应用特点、语言内在特点和对客观系统的描述三个不同的角度来对高级语言进行分类。
1)、从应用特点的角度来分 (1)基础语言(例:FORTRAN,BASIC,COBOL和ALGOL) (2)结构化语言(例:ALGOL,PL/1,PASCAL,C,Ada) (3)专用语言(例:APL,LISP,PROLOG,BLISS,FORTH)
2)、从语言内在特点的角度来分 (1)系统实现语言(例:C语言) (2)静态高级语言(例:FORTRAN、COBOL) (3)块结构高级语言(例:PASCAL,ALGOL) (4)动态高级语言
3)、从描述客观系统的角度来分 (1)面向过程语言 数据结构+算法 (2)面向对象语言 对象+消息 例:Delphi、Visual Basic、JAVA、C++。
2程序设计语言的特点 1.名字说明 2.类型说明 3.选择控制结构 4.循环控制结构 5.程序对象的局部性 6.变量的局部共享 7.异常处理 8.独立编译
3程序设计语言的选择 一般情况下,我们采用高级语言来编程。 选择具体高级语言类型的原则: (1)系统的应用领域 (2)用户的要求 (3)软件的执行环境 (4)目标系统的性能要求 (5)程序员的知识水平 (6)软件的可移植性要求
7.1.2 编码风格 1 程序内部的文档 程序内部的文档包括: 1) 恰当的标识符(变量和标号)的名字; 2) 适当的注释; 3) 程序的视觉组织。
2 数据说明 为了使数据更容易理解和维护,应遵循一些简单的原则: (1)数据说明的次序应当规范化。 (2)当多个变量名在一个语句中说明时,应该按字母顺序排列这些变量。 (3)如果设计时使用了一个复杂的数据结构,则应注解说明用程序设计语言实现这个数据结构的方法和特点。
3 语句构造 语句构造应遵循的原则是:每条语句应该简单而直接,不应为了片面追求效率而使代码变得过于复杂。 人们在长期的实践中总结了以下一些规则: 不要为了节省空间而把多个语句写在同一行; 用空格或可读的符号使语句的内容更加清晰; 尽量避免复杂的条件测试; 尽量避免使用“非”条件的条件语句; 避免过多使用循环嵌套和条件嵌套; 利用括号使逻辑表达式或算术表达式的运算次序清晰直观; 尽可能使用库函数; 让编译程序作简单的优化。
4 输入/输出 在设计和程序编码时,应考虑输入和输出风格原则: 对所有输入数据都进行校验,以保证每个数据的有效性; 检查重要的输入项组合的合法性; 使得输入的步骤和操作尽可能简单,并保持简单的输入格式; 输入一批数据时,使用输入结束指示符,不要要求用户说明输入项数; 在以交互式输入/输出方式进行输入时,要指明可以使用的选择值或界限值; 应允许缺省值; 当程序设计语言对输入/输出格式有严格要求时,应保持输入格式与输入语句的要求一致; 给所有的输出加注释,并设计输出报表格式。
5 效率 通常,效率主要指占用处理机时间和主存区域两个方面。好的编码可以提高效率,在我们进一步讨论这个问题之前,应该记住三条原则: 第一、效率是一个性能要求,因而应该在需求分析阶段确定代码效率方面的要求; 第二、通过好的设计可以提高效率; 第三、程序的效率和程序的简明程度是一致的,不应该为了提高代码效率而牺牲程序的清晰性和可读性。
1)、代码效率 2)、存储效率 3)、输入/输出的效率
编码工具 为了提高编码的效率,保证程序的可靠性,我们经常使用一些编码工具。 首先要用的当然是编辑工具了。选用合适的编辑工具可以大大方便编程,提高效率。 编译程序的好坏也会影响编码的效率。一方面,好的编译程序应该是程序员的好助手,能够帮助程序员及时准确地诊断出程序中的差错,减少程序开发的成本。另一方面,编译程序还应该能够生成高效率的机器代码,也就是代码优化。
7.2 软件测试基础 关于测试目的,G.Myers给出了以下的观点: (1)测试是为了发现程序中的错误而执行程序的过程; (2)好的测试方案是尽可能发现迄今为止尚未发现的错误的测试方案; (3)成功的测试是发现了至今为止尚未发现的错误的测试。
7.2.1 软件测试的定义 测试的定义:为了发现程序中的错误而执行程序的过程。具体地说,软件测试是根据软件开发各阶段的规格说明和程序的内部结构而精心设计出一批测试用例,并利用测试用例来运行程序,以发现程序错误的过程。
7.2.2软件测试的基本原则 (1)尽早地、不断地进行软件测试。 (2)设计测试用例时,要给出测试的预期结果。 (3)开发小组和测试小组分开。 (4)要设计非法输入的测试用例。 (5)在对程序修改之后要进行回归测试。 (6)程序中尚未发现的错误的数量往往与在该段程序中已发现的错误的数量成正比。
7.2.3软件测试的方法 测试任何产品都有两种方法: 如果已经知道了产品应该具有的功能,可以通过测试来检验是否每个功能都能正常使用,这种方法称为黑盒测试; 如果知道产品的内部工作过程,可以通过测试来检验产品内部动作是否按照规格说明书的规定正常进行,这种方法称为白盒测试。
7.2.4软件测试的步骤 大型软件系统通常由若干个子系统组成,每个子系统又由许多模块组成,因此,测试过程也必须分步骤进行,后一个步骤在逻辑上是前一个步骤的继续。 大型软件系统的测试过程基本上由下述几个步骤组成。
1.单元测试 又称模块测试。每个程序模块完成一个相对独立的子功能,所以可以对该模块进行单独的测试。由于每个模块都有清晰定义的功能,所以通常比较容易设计相应的测试方案,以检验每个模块的正确性。
2. 子系统测试 子系统测试是把经过单元测试的模块放在一起形成一个子系统来测试。模块相互间的协调和通信是这个测试过程中的主要问题,因此,这个步骤着重测试模块的接口。
3. 系统测试 系统测试是把经过测试的子系统装配成一个完整的系统来测试。在这个过程中不仅应该发现设计和编码的错误,还应该验证系统确实能提供需求说明书中指定的功能。在这个测试步骤中发现的往往是软件设计中的错误,也可能发现需求说明中的错误。 不论是子系统测试还是系统测试,都兼有检测和组装两重含义,通常称为集成测试。
4. 验收测试 验收测试把软件系统作为单一的实体进行测试,测试内容与系统测试基本类似,但是它是在用户积极参与下进行的,而且可能主要使用实际数据(系统将来要处理的信息)进行测试。验收测试的目的是验证系统确实能够满足用户的需要,在这个测试步骤中发现的往往是系统需求说明书中的错误。验收测试也称为确认测试。
5. 平行运行 所谓平行运行就是同时运行新开发出来的系统和将被它取代的旧系统,以便比较新旧两个系统的处理结果。 这样做的具体目的有如下几点: (1) 可以在准生产环境中运行新系统而又不冒风险; (2) 用户能有一段熟悉新系统的时间; (3) 可以验证用户指南和使用手册之类的文档; (4) 能够以准生产模式对新系统进行全负荷测试,可以用测试结果验证性能指标。
软件测试的步骤 7.3 单元测试 单元测试又称模块测试,集中对软件设计的最小单位——模块进行测试,主要是为了发现模块内部可能存在的各种错误和不足。 进行单元测试时,根据程序的内部结构设计测试用例,主要使用白盒测试法。由于各模块间相对独立,因而对多个模块的测试可以并行地进行,以提高测试效率。
1、单元测试的内容 (1)模块接口 主要进行的测试项目有以下几方面: 所测模块的形式参数和调用该模块的实际输入参数在参数数目、属性和顺序上是否匹配; 输出给被调用模块的参数在数目、属性和顺序上是否正确; 全局变量的定义和用法在各个模块中是否一致。
(2)局部数据结构 模块的局部数据结构是常见的错误来源,测试者应该仔细设计测试用例,以便发现这样一些类型的错误: 错误的变量名; 错误的或不一致的数据类型说明; 使用尚未赋值或尚未初始化的变量; 错误的初始值或错误的缺省值; 数据类型不相容;
(3)重要的执行路径 选择适当的测试用例,对模块中的最有代表性、最可能发现错误的执行路径进行测试。
(4)出错处理 由于输入等条件的限制,程序在运行中出错往往是不可避免的。因而好的程序设计应该能预见可能出现的各种出错情况,并且设置相应的出错处理,以便在出现错误时执行相应的操作。 在单元测试时也应该对模块中的出错处理部分进行测试,进行这一部分测试时可能存在的错误主要有: 对错误的描述难于理解,或者是描述过于简单; 显示的错误信息与实际错误不相符; 在对错误进行处理之前,错误条件已经引起系统的干预; 对错误的处理不正确。
(5)边界条件 我们知道,软件常常在它的边界上失效。例如,处理n元数组的第一个元素或最后一个元素时,在n次循环中的第n次重复时,往往会发生错误。因此,使用刚好小于、等于或大于最大值或最小值的数据结构、控制量和数据值的测试方案时,很可能会发现软件中的错误。
2、单元测试的步骤 单元测试的对象是模块。测试者必须自己动手设计这两类模块:驱动模块和存根模块。 驱动模块:相当于所测模块的“主程序”。它接收测试数据,把这些数据传送给所测模块,然后输出测试结果。 存根模块:也叫虚拟子程序。它的作用是模拟被测模块所调用的子模块。存根模块可以做少量的数据操作,一般情况下,不需要把实际子模块的所有功能都带进来。
7.4 集成测试 集成测试过程中要考虑的问题: (1)数据穿过模块接口时是否会丢失; (2)模块的功能是否会对其它模块的功能产生不利的影响; (3)把子功能组合起来,能否达到预期的主功能要求; (4)单个模块的误差累积起来是否会放大到不能接受的程度; (5)全局数据结构是否有问题。
将各个模块组装成系统的方法:非增殖式组装方式和增殖式组装方式。将各个模块组装成系统的方法:非增殖式组装方式和增殖式组装方式。 采用非增殖式组装方式:先分别对每个模块进行测试,再把所有模块按设计要求组装在一起进行测试,最终得到所要求的软件。 采用增殖式组装方式:把下一个要测试的模块同已经测试好的那些模块结合起来进行测试,测试完以后再把下一个应该测试的模块结合进来测试,这种方法实际上同时完成单元测试和集成测试。
这两种方法各有优缺点: (1)采用非增殖式组装方式时,可以较早发现模块间的接口错误,而采用增殖式组装方式时,只有在模块加进来时才可能发现,因此接口错误发现较晚。 (2)采用非增殖式组装方式时要对每个模块进行单元测试,需要编写的测试软件较多,工作量大,而采用增殖式组装方式时,利用已测试过的模块部分作为部分测试软件,因而工作量较小。 (3)非增殖式组装方式要求一下子把所有模块组装起来,如果发现错误则较难判断错误的位置,而采用增殖式组装方式时,由于每次只加入一个模块,因而错误往往与刚加入的模块有关,查错则相对容易些。 (4)采用非增殖式组装方式时,各模块的单元测试可以并行地进行,因此可以充分利用人力,加快测试进程,采用增殖式组装方式时却不能如此。
在使用增殖式组装方式时,常用的有自顶向下和自底向上两种方法。在使用增殖式组装方式时,常用的有自顶向下和自底向上两种方法。 1、自顶向下结合 采用这种组装方式时,是从主控制模块开始,沿着软件的控制层次向下移动,从而逐渐把各个模块都结合起来。 左图是一个树形结构,主控制模块是M1,在把主控制模块M1所属的那些模块都组装起来时可以采取两种方法:深度优先策略或者宽度优先策略。
采用深度优先的结合方法时,先把软件结构的一条主控制通路上的所有模块一个一个地结合组装起来。主控制通路的选择取决于应用的特点。对于图8.5来说,如果选取左通路为主控通路,那么首先结合模块M1,M2和M5,然后是M8。如果M2的某个功能需要的话,可结合M6。然后结合中间的和右边的控制通路。采用深度优先的结合方法时,先把软件结构的一条主控制通路上的所有模块一个一个地结合组装起来。主控制通路的选择取决于应用的特点。对于图8.5来说,如果选取左通路为主控通路,那么首先结合模块M1,M2和M5,然后是M8。如果M2的某个功能需要的话,可结合M6。然后结合中间的和右边的控制通路。 采用宽度优先的结合方法时,逐层结合直接下属的所有模块,即把处于同一个控制层次上的所有模块组装起来。对于图8.5来说,首先结合模块M2,M3和M4(代替存根模块S4),接着结合下一个控制层次中的模块M5,M6和M7;如此继续进行下去,直到所有模块都被结合进来。
不管是采用深度优先策略还是宽度优先策略,其结合过程如下:不管是采用深度优先策略还是宽度优先策略,其结合过程如下: (1)用主控制模块作为测试驱动模块,所有直接下属于主控制模块的模块用存根模块代替,对主模块进行测试; (2)根据选定的结合策略(深度优先或宽度优先),每次用一个实际模块替换一个存根模块,对新结合进来的模块的直接下属模块,用新的存根模块代替; (3)对结合进来的模块进行相应的测试; (4)为了保证新加入的模块不引入新的错误,可以进行回归测试,即重复以前进行过的部分测试或全部测试。 从第(2)步开始,不断地重复进行上述过程,直到所有模块都结合进来为止。
采用自顶向下的结合策略的好处: 在测试过程中能够较早地对主要的控制或关键的判断点进行检验。因为在一个功能划分合理的软件结构中,关键的判断点常常出现在较高的层次里,所以能够较早碰到。如果主要控制存在问题,及早发现这类问题并尽快想办法解决是十分重要的,这样可以大大减少后面的工作量。如果选择的是深度优先结合方法,可以首先实现并验证软件的一个比较完整的功能,这样对增强开发人员和用户双方的信心是很有意义的。
采用自顶向下的结合策略的不足: 可能会遇到逻辑上的问题。当我们为了充分地测试较高层次的功能时,可能需要较低层次上处理的信息,但是我们采用自顶向下的方法时,存根模块代替了低层次的模块,若高层模块需要低层模块返回的信息不仅数量大,而且种类也很多时,存根模块有可能很难完全满足这个要求,因而,这种方法有一定的局限性。为了解决这个问题,可以采用以下解决办法: (1)把许多测试推迟到用实际模块替换了存根模块以后再进行。采用这种方法也有一定的缺陷:由于我们对一些特定的测试和组装与特定模块间的对应关系失去了某些控制,从而在确定错误原因时会发生困难。 (2)由层次系统的底部向上组装软件。这种方法就是下面要介绍的自底向上结合方法。
2、自底向上结合 自底向上测试是从软件结构最低层的模块开始进行组装和测试。它不需要存根模块,但需要驱动模块。其结合过程如下: (1)把低层模块组合成实现某个特定软件子功能的模块族; (2)为每一个族编写一个驱动模块,作为测试的控制来协调测试用例的输入和输出; (3)对模块族进行测试; (4)按模块结构图依次向上扩展,用实际模块替换驱动模块,将模块族与新的模块结合,形成新的模块族,再进行测试,直到所有模块都被结合进来。
图中自底向上的结合过程:首先把模块组合成族1、族2和族3,然后设计相应的驱动模块D1、D2和D3,并对每个子功能族进行测试;族1和族2上属于模块Ma,去掉驱动模块D1和D2,把这两个族直接与Ma结合,同样地,在族3与模块Mb结合之前将D3去掉;最后Ma和Mb与Mc结合起来。图中自底向上的结合过程:首先把模块组合成族1、族2和族3,然后设计相应的驱动模块D1、D2和D3,并对每个子功能族进行测试;族1和族2上属于模块Ma,去掉驱动模块D1和D2,把这两个族直接与Ma结合,同样地,在族3与模块Mb结合之前将D3去掉;最后Ma和Mb与Mc结合起来。
自顶向下结合的主要优点:不需要设计测试驱动模块,与存根模块相联系的问题可能在测试的早期发现。主要缺点是:需要设计存根模块,并且由于为了使存根模块能够尽量模拟实际模块的功能,必然会增加设计存根模块的复杂度,从而导致增加一些附加的测试。自顶向下结合的主要优点:不需要设计测试驱动模块,与存根模块相联系的问题可能在测试的早期发现。主要缺点是:需要设计存根模块,并且由于为了使存根模块能够尽量模拟实际模块的功能,必然会增加设计存根模块的复杂度,从而导致增加一些附加的测试。 自底向上结合的主要优点:不需要设计存根模块,而设计测试驱动模块一般比建立存根模块要容易,同时比较容易设计测试用例,并且可以实现多个模块的并行测试,从而提高测试效率。主要缺点是:直到最后一个模块结合进来以前,程序作为一个整体始终不存在。也就是说,对主要的控制直到最后才接触到。 一般来说,通常根据情况结合这两种方法来进行组装和测试:对软件结构中较上层模块使用自顶向下结合方法,对软件结构中较下层模块使用自底向上结合方法。
7.5 确认测试 确认测试也称为验收测试,它的目标是验证软件的有效性。 软件有效性的一个简单定义是: 如果软件的功能和性能如同用户所合理期待的那样,软件就是有效的。 需求分析阶段产生的软件需求规格说明书,准确地描述了用户对软件的合理期望,因此是软件有效性的标准,也是进行确认测试的基础。
系统测试 软件仅仅是计算机系统的一个组成部分,在实际运行中,它要和计算机系统的其它元素一起工作,所以最终要把软件与其它系统元素结合起来,进行一系列的集成测试和有效性测试。 系统测试的目的在于通过与系统的需求定义作比较,发现软件与系统定义不符合或与之矛盾的地方。
软件测试方法 任何产品都可以使用以下两种方法进行测试: (1)如果已知产品的功能,则可以对它的每一个功能进行测试,看是否都达到了预期的要求; (2)如果已知产品的内部工作过程,则可以对它的每种内部操作进行测试,看是否符合设计要求。 第一种方法是黑盒测试,第二种方法是白盒测试。
7.6 白盒测试 白盒测试时将程序看作是一个透明的盒子,也就是说测试人员完全了解程序的内部结构和处理过程。所以测试时按照程序内部的逻辑测试程序、检验程序中的每条通路是否都能按预定的要求正确工作。 利用白盒测试设计测试用例时,应包括以下三类测试: (1)语句测试:要求程序中的每个语句至少测试一次; (2)分支测试:要求程序中的每个分支至少测试一次; (3)路径测试:要求程序中的每条路径至少测试一次。
7.6.1 逻辑覆盖 逻辑覆盖是以程序的内部逻辑结构为基础的测试用例设计技术,属于白盒测试。它要求测试人员十分清楚程序的逻辑结构,考虑的是测试用例对程序内部逻辑覆盖的程度。 根据覆盖的目标,逻辑覆盖又可以分为: 语句覆盖 判定覆盖 条件覆盖 判定/条件覆盖 条件组合覆盖 点覆盖 边覆盖 路径覆盖