在介绍软件测试之前,首先应该明确 “ 错误 ” ( error )和 “ 缺陷 ” ( fault )的概念。根据 IEEE 的定义, “ 错误 ” 主要针对软件开发过程, “ 缺陷 ” 主要针对软件产品。
软件开发人员在软件开发过程(主要是分析 、 设计和编码过程)中所出现的 “ 错误 ” 是导致软件产品 “ 缺陷 ” 的原因,反过来说, “ 缺陷 ” 是 “ 错误 ” 的结果和表现形式。
软件测试的目的就是在软件投入生产性运行之前,尽可能多地发现软件产品(主要是指程序)中的错误(缺陷)。为了发现软件中的错误(缺陷),应竭力设计能暴露错误(缺陷)的测试用例。
测试用例是由测试数据和预期结果构成的。一个好的测试用例是极有可能发现至今为止尚未发现的错误(缺陷)的测试用例。一次成功的测试是发现了至今为止尚未发现的错误(缺陷)的测试。
高效的测试是指用少量的测试用例,发现被测软件尽可能多的错误(缺陷)。软件测试所追求的目标就是以尽可能少的时间和人力发现软件产品中尽可能多的错误(缺陷)。
1 软件测试阶段
从测试阶段上分,软件测试通常可分为单元测试、集成测试和系统测试。
(1)单元测试
单元测试( unit testing ),也称模块测试,通常可放在编程阶段,由程序员对自己编写的模块自行测试,检查模块是否实现了详细设计说明书中规定的功能和算法。
单元测试主要发现编程和详细设计中产生的错误,单元测试计划应该在详细设计阶段制定。
单元测试期间着重从以下几个方面对模块进行测试:模块接口 、 局部数据结构 、 重要的执行通路 、 出错处理通路和边界条件等。
测试一个模块时需要为该模块编写一个驱动模块和若干个桩( stub )模块。
- 驱动模块用来调用被测模块,它接收测试者提供的测试数据,并把这些数据传送给被测模块,然后从被测模块接收测试结果,并以某种可以看见的方式(例如显示或打印)将测试结果返回给测试者。
- 桩模块用来模拟被测模块所调用的子模块,它接受被测模块的调用,检验调用参数,并以尽可能简单的操作模拟被调用的子程序模块功能,把结果送回被测模块。
顶层模块测试时不需要驱动模块,底层模块测试时不需要桩模块。模块的内聚程度高可以简化单元测试过程。如果每个模块只完成一种功能,则需要的测试方案数目将明显减少,模块中的错误也更容易预测和发现。
(2)集成测试
集成测试( integration testing ),也称组装测试,它是对由各模块组装而成的程序进行测试,主要目标是发现模块间的接口和通信问题。例如,数据穿过接口可能丢失,一个模块对另一个模块可能由于疏忽而造成有害影响,把子功能组合起来可能不产生预期的主功能,个别看来是可以接受的误差可能积累到不能接受的程度,全程数据结构可能有问题等。
集成测试主要发现设计阶段产生的错误,集成测试计划应该在概要设计阶段制订。
集成的方式可分为非渐增式和渐增式。
- 非渐增式集成是先测试所有的模块,然后一下子把所有这些模块集成到一起,并把庞大的程序作为一个整体来测试。这种测试方法的出发点是可以 “ 一步到位 ” ,但测试者面对众多的错误现象,往往难以分清哪些是 “ 真正的 ” 错误,哪些是由其他错误引起的 “ 假性错误 ” ,诊断定位和改正错误也十分困难。非渐增式集成只适合一些非常小的软件。
- 渐增式集成是将单元测试和集成测试合并到一起,它根据模块结构图,按某种次序选一个尚未测试的模块,把它同已经测试好的模块组合在一起进行测试,每次增加一个模块,直到所有模块被集成在程序中。这种测试方法比较容易定位和改正错误,目前在进行集成测试时已普遍采用渐增式集成。
渐增式集成又可分为自顶向下集成和自底向上集成。自顶向下集成先测试上层模块,再测试下层模块。由于测试下层模块时它的上层模块已测试过,所以不必另外编写驱动模块。自底向上集成先测试下层模块,再测试上层模块。同样,由于测试上层模块时它的下层模块已测试过,所以不必另外编写桩模块。
这两种集成方法各有利弊,一种方法的优点恰好对应于另一种方法的缺点,实际测试时可根据软件特点及进度安排灵活选用最适当的方法,也可将两种方法混合使用。
(3)系统测试
系统测试是软件测试中的最后的 、 最完整的测试,它是在单元测试和集成测试的基础上进行的,它从全局来考察软件系统的功能和性能要求。系统测试计划应该在需求分析阶段制订。
通常,系统测试包括确认测试和验收测试。
- 确认测试,主要依据软件需求说明书检查软件的功能 、 性能及其他特征是否与用户的需求一致。软件配置复查是确认测试的另一项重要内容。复查的目的是保证软件配置的所有成分都已齐全,质量符合要求,文档与程序完全一致,具有完成软件维护所必需的细节。
- 验收测试,如果一个软件是为某个客户定制的,最后还要由该客户来实施验收测试,以便确认其所有需求是否都已得到满足。由于软件系统的复杂性,在实际工作中,验收测试可能会持续到用户实际使用该软件之后的相当长的一段时间。如果一个软件是作为产品被许多客户使用的,不可能也没必要由每个客户进行验收测试。
绝大多数软件开发商都使用被称为 (Alpha) 测试和 (Beta) 测试的过程,来发现那些看起来只有最终用户才能发现的错误 。
- Alpha 测试由用户在开发者的场所进行,并且在开发者的指导下进行测试。开发者负责记录发现的错误和使用中遇到的问题。也就是说,测试是在 “ 受控的 ” 环境中进行的 。
- Beta 测试是在一个或多个用户的现场由该软件的最终用户实施的,开发者通常不在现场,用户负责记录发现的错误和使用中遇到的问题并把这些问题报告给开发者。也就是说,测试是在 “ 不受控的 ” 环境中进行的。经过系统测试之后的软件通常就可以交付使用了。
2 白盒测试和黑盒测试
从测试方法上分,软件测试可分为白盒测试和黑盒测试。
2.1 白盒测试
白盒测试,又称结构测试,主要用于单元测试阶段。它的前提是可以把程序看成装在一个透明的白箱子里,测试者完全知道程序的结构和处理算法。这种方法按照程序内部逻辑设计测试用例,检测程序中的主要执行通路是否都能按预定要求正常工作。
白盒测试根据软件的内部逻辑设计测试用例,常用的技术是逻辑覆盖,即考察用测试数据运行被测程序时对程序逻辑的覆盖程度。主要的覆盖标准有6种:语句覆盖 、 判定覆盖 、 条件覆盖 、 判定 / 条件覆盖 、 组合条件覆盖和路径覆盖。
(1)语句覆盖
语句覆盖是指选择足够多的测试用例,使得运行这些测试用例时,被 测程序的每个语句至少执行一次。
很显然,语句覆盖是一种很弱的覆盖标准。
(2)判定覆盖
判定覆盖又称分支覆盖,它的含义是,不仅每个语句至少执行一次,而且每个判定的每种可能的结果(分支)都至少执行一次。判定覆盖比语句覆盖强,但对程序逻辑的覆盖程度仍然不高。
(3)条件覆盖
条件覆盖的含义是,不仅每个语句至少执行一次,而且使判定表达式中的每个条件都取得各种可能的结果。
条件覆盖不一定包含判定覆盖,判定覆盖也不一定包含条件覆盖。
(4)判定/条件覆盖
同时满足判定覆盖和条件覆盖的逻辑覆盖称为判定 / 条件覆盖。它的含义是,选取足够的测试用例,使得判定表达式中每个条件的所有可能结果至少出现一次,而且每个判定本身的所有可能结果也至少出现一次。
(5)条件组合覆盖
条件组合覆盖的含义是,选取足够的测试用例,使得每个判定表达式中条件结果的所有可能组合至少出现一次。显然,满足条件组合覆盖的测试用例,也一定满足判定 / 条件覆盖。
因此,条件组合覆盖是上述5种覆盖标准中最强的一种。然而,条件组合覆盖还不能保证程序中所有可能的路径都至少经过一次。
(6)路径覆盖
路径覆盖。路径覆盖的含义是,选取足够的测试用例,使得程序的每条可能执行到的路径都至少经过一次(如果程序中有环路,则要求每条环路路径至少经过一次)。
路径覆盖实际上考虑了程序中各种判定结果的所有可能组合,因此是一种较强的覆盖标准。但路径覆盖并未考虑判定中的条件结果的组合,并不能代替条件覆盖和条件组合覆盖。
2.2 黑盒测试
黑盒测试,又称功能测试,主要用于集成测试和确认测试阶段。它把软件看作一个不透明的黑箱子,完全不考虑(或不了解)软件的内部结构和处理算法,它只检查软件功能是否能按照软件需求说明书的要求正常使用,软件是否能适当地接收输入数据并产生正确的输出信息,软件运行过程中能否保持外部信息(例如文件和数据库)的完整性等。
黑盒测试根据软件需求说明书所规定的功能来设计测试用例,它不考虑软件的内部结构和处理算法。常用的黑盒测试技术包括等价类划分 、 边值分析 、 错误推测和因果图等。
(1)等价类划分
在设计测试用例时,等价类划分是用得最多的一种黑盒测试方法。所谓等价类就是某个输入域的集合,对于一个等价类中的输入值来说,它们揭示程序中错误的作用是等效的。也就是说,如果等价类中的一个输入数据能检测出一个错误,那么等价类中的其他输入数据也能检测出同一个错误;反之,如果等价类中的一个输入数据不能检测出某个错误,那么等价类中的其他输入数据也不能检测出这一错误(除非这个等价类的某个子集还属于另一等价类)。
如果一个等价类内的数据是符合(软件需求说明书)要求的 、 合理的数据,则称这个等价类为有效等价类。有效等价类主要用来检验软件是否实现了软件需求说明书中规定的功能。
如果一个等价类内的数据是不符合(软件需求说明书)要求的 、 不合理或非法的数据,则称这个等价类为无效等价类。无效等价类主要用来检验软件的容错性。
黑盒测试中,利用等价类划分方法设计测试用例的步骤是:
① 根据软件的功能说明,对每一个输入条件确定若干个有效等价类和若干个无效等价类,并为每个有效等价类和无效等价类编号 。
② 设计一个测试用例,使其覆盖尽可能多的尚未被覆盖的有效等价类。重复这一步,直至所有的有效等价类均被覆盖 。
③ 设计一个测试用例,使其覆盖一个尚未被覆盖的无效等价类。重复这一步,直至所有的无效等价类均被覆盖。
无效等价类是用来测试非正常的输入数据的,因此每个无效等价类 都有可能查出软件中的错误,所以要为每个无效等价类设计一个测试用例。
(2)边值分析
经验表明,软件在处理边界情况时最容易出错。设计一些测试用例,使软件恰好运行在边界附近,暴露出软件错误的可能性会更大一些。
通常,每一个等价类的边界,都应该着重测试,选取的测试数据应该恰好等于 、 稍小于或稍大于边界值。
将等价类划分法和边值分析法结合使用,更有可能发现软件中的错误。
(3)错误推测
使用等价类划分和边值分析技术,有助于设计出具有代表性的 、 容易暴露软件错误的测试方案。但是,不同类型不同特定的软件通常又有一些特殊的容易出错的地方。
错误推测法主要依靠测试人员的经验和直觉,从各种可能的测试方案中选出一些最可能引起程序出错的方案。
(4)因果图
因果图法是根据输入条件与输出结果之间的因果关系来设计测试用例的,它首先检查输入条件的各种组合情况,并找出输出结果对输入条件的依赖关系,然后为每种输出条件的组合设计测试用例。
3 缺陷的分类和级别
根据 IEEE 标准和 PaulC.Jorgensen 的教科书,软件测试中所发现的错误(缺陷)主要包括以下几类:
(1)输入 / 输出错误。包括不接收正确的输入 、 接收不正确的输入 、 描述有错或遗漏 、 参数有错或遗漏 、 输出结果有误 、 输出格式有误 、 输出时间有误 、 结果不一致 、 遗漏结果 、 不合逻辑的结果 、 拼写 / 语法错误 、 修饰词错误。
(2)逻辑错误。包括遗漏情况 、 重复情况 、 极端条件出错 、 解释有误 、 遗漏条件 、 外部条件有错 、 错误变量的测试 、 不正确的循环迭代 、 错误的操作符。
(3)计算错误。包括不正确的算法 、 遗漏计算 、 不正确的操作数 、 不正确的操作 、 括号错误 、 精度不够错误的内置函数。
(4)接口错误。包括不正确的中断处理 、 I / O时序有错 、 调用了错误的过程 、 调用了不存在的过程 、 参数不匹配 、 不兼容的类型 、 过量的包含。
(5)数据错误。包括不正确的初始化 、 不正确的存储 / 访问 、 错误的标识 / 索引值 、 不正确的打包 / 拆包 、 使用了错误的变量 、 错误的数据引用 、 缩放数据范围或单位错误 、 不正确的数据维数 、 不正确的下标 、 不正确的类型 、 不正确的数据范围 、 数据超出限制 、 数据溢出 、 不一致的数据。
根据错误(缺陷)后果的严重程度,Beizer 将错误(缺陷)分为 10 级:
(1)轻微(例如,界面文字有个别的错别字,但不影响理解)。
(2)中等(例如,界面文字错误可能误导操作者)。
(3)使人不悦(例如,数字串被断开)。
(4)影响使用(例如,有些交易没有处理)。
(5)严重(例如,丢失交易)。
(6)非常严重(例如,不正确的交易处理)。
(7)极为严重(例如,经常出现不正确的交易处理)。
(8)无法容忍(例如,数据库遭到破坏)。
( 9 )灾难性(例如,系统无法工作)。
( 10 )传染性(例如,可导致其他系统无法工作)。
4 调试
调试又称为排错,调试与成功的测试形影相随。测试成功的标志是发现了错误。根据错误迹象确定错误的原因和准确位置并加以改正,主要依靠排错技术。
调试是一个相当艰苦的过程,究其原因除了开发人员心理方面的障碍外,还因为隐藏在程序中的错误具有下列特殊的性质:
(1)错误的外部征兆远离引起错误的内部原因,对于高度耦合的程序结构此类现象更为严重。
(2)纠正一个错误造成了另一错误现象(暂时)的消失。
(3)某些错误征兆只是假象。
(4)因操作人员一时疏忽造成的某些错误征兆不易追踪。
(5)错误是由于分时而不是程序引起的。
(6)输入条件难以精确地再构造(例如,某些实时应用的输入次序不确定)。
(7)错误征兆时有时无,此现象对嵌入式系统尤其普遍。
(8)错误是由于把任务分布在若干台不同处理机上运行而造成的。
在软件排错过程中,可能遇到大大小小 、 形形色色的问题,随着问题的增多,排错人员的压力也随之增大,过分的紧张致使开发人员在排除一个问题的同时又引入更多的新问题。尽管排错不是一门好学的技术,但还是有若干行之有效的方法和策略的,
常用的排错策略分为三类:
(1)原始类。原始类排错方法是最常用也是最低效的方法,只有在万般无奈的情况下才使用它,主要思想是 “ 通过计算机找错 ” 。 例如输出存储器 、 寄存器的内容,在程序安排若干输出语句等,凭借大量的现场信息,从中找到出错误的线索。虽然最终也能成功,但难免要耗费大量的时间和精力。
(2)回溯类。回溯法能成功地用于程序的排错。方法是从出现错误征兆处开始,人工地沿控制流程往回追踪,直至发现出错的根源,不幸的是程序变大后,可能的回溯路线显着增加,以致人工进行完全回溯可望而不可即。
(3)排除类。排除法基于归纳和演绎原理,采用 “ 分治 ” 的概念,首先分析与错误出现有关的所有数据,假想一个错误原因,用这些数据证明或反驳它;或者一次列出所有可能的原因,通过测试一一排除。只要某次测试结果说明某种假设已呈现端倪,则立即精化数据,乘胜追击。