第一章 一个自我评价测试
不要只是为了证明程序能够正确运行而去测试程序;相反, 应该一开始就假设程序中隐藏着错误(这种假设对于几乎所有的程序都成立),然 后测试程序,发现尽可能多的错误。
第二章 软件测试的心理学和经济学
如果在测试某段程序时发现了错误,而且这些错误是可以修复的,就将这次合理设 计并得到有效执行的测试称作是“成功的。
如果本次测试可以最终确定再无其他 可查出的错误,同样也被称作是“成功的”。
黑盒测试和 白盒测试是两种最普遍的策略
黑盒测试是一种重要的测试策略,又称为数据驱动的测试或输入/输出驱动的测 试。使用这种测试方法时,将程序视为一个黑盒子。测试目标与程序的内部机制和 结构完全无关,而是将重点集中放在发现程序不按其规范正确运行的环境条件
测试数据完全来源于软件规范(换句话说,不需要去了解程序 的内部结构)。
所有可能的输入条件都作为测试用例。
穷举路径测试就如同穷举输入测试,非但不可能,也 是不切实际的。 “穷举路径测试即完全的测试”论断存在的第二个问题是,虽然我们可以测试 到程序中的所有路径,但是程序可能仍然存在着错误。这有三个原因。 第一,即使是穷举路径测试也决不能保证程序符合其设计规范。举例来说,如 果要编写一个升序排序程序,但却错误地编成了一个降序排序程序,那么穷举路径 测试就没多大价值了;程序仍然存在着一个缺陷:它是个错误的程序因为不符合设 计的规范。 第二,程序可能会因为缺少某些路径而存在问题。穷举路径测试当然不能发现 缺少了哪些必需路径。第三,穷举路径测试可能不会暴露数据敏感错误。
原则 l:测试用例 中一个必需部分是对预期输出或结果的定义
原则2:程序员应当避免测试自己编写的程序.
原则 3:编写软件的组织不应当测试自己编写的软件
原则 4:应当彻底检查每个测试的执行结果
原则 5:测试用例的编写不仅应当根据有效和预期的输入情况,而且也应当根据无 效和未预料到的输入情况
原则 6:检查程序是否“未做其应该做的”仅是测试的一半,测试的另一半是检查 程序是否“做了其不应该做的
原则 7:应避免测试用例用后即弃,除非软件本身就是一个一次性的软件。
软件测试是为发现错误而执行程序的过程。 • 一个好的测试用例具有较高的发现某个尚未发现的错误的可能性。 • 一个成功的测试用例能够发现某个尚未发现的
一个测试用例必须包括 两个部分: 1.对程序的输入数据的描述。 2.对程序在上述输入数据下的正确输出结果的精确描述。
第三章 代码检查、走查与评审
这个代码检查过程通常将注意力集中在发现错误上,而不是纠正错 误。
大多数的代码检查都是按每小时大约阅读 150 行代码的速度进行。
程序员必须怀着非自我本位的态度来对待检查过程,对整个过程采取积极和 建设性的态度:代码检查的目标是发现程序中的错误,从而改进软件的质量。正因 为这个原因,大多数人建议应对代码检查的结果进行保密,仪限于参与者范围内部
代码检查过程的一个重要部分就是对照一份错误列表,来检查程序是否存在常 见错误
1.是否有引用的变量未赋值或未初始化?这可能是最常见的编程错误
2.对于所有的数组引用,是否每一个下标的值都在相应维规定的界限之内? 3.对于所有的数组引用,是否每一个下标的值都是整数?虽然在某些语言中这 不是错误,但这样做是危险的。 4.对于所有的通过指针或引用变量的引用,当前引用的内存单元是否分配?这 就是所谓的“虚调用(dangling reference)”错误。当指针的生命期大于所引用内存 单元的生命期时,错误就会发生。5.如果一个内存区域具有不同属性的别名,当通过别名进行引用时,内存区域 中的数据值是否具有正确的属性。6.变量值的类型或属性是否与编译器所预期的一致?7.在使用的计算机上,当内存分配的单元小于内存可寻址的单元大小时,是否 存在直接或间接的寻址错误?8..当使用指针或引用变量时,被引用的内存的属性是否与编译器所预期的一 致?9.假如一个数据结构在多个过程或子程序中被引用,那么每个过程或子程序对 该结构的定义是否都相同。 10.如果字符串有索引,当对数组进行索引操作或下标引用,字符串的边界取 值是否有“仅差一个(off-by-one)”的错误? 11.对于面向对象的语言,是否所有的继承需求都在实现类中得到了满足?
数据声明错误
1.是否所有的变量都进行了明确的声明?
2.如果变量所有的属性在声明中没有明确说明,那么默认的属性能否被正确理 解
3.如果变量在声明语句中被初始化,那么它的初始化是否正确? 在很多语言 中,数组和字符串的初始化比较复杂,因此也成为容易出错的地方。 4.是否每个变量都被赋予了正确的长度和数据类型? 5.变量的初始化是否与其存储空间的类型一致?举例来说,如果 FORTRAN 语 言子程序中的一个变量在每次调用子程序时都需要重新初始化一次,那么必须使用 赋值语句对其初始化,而不应该用 DATA 语句。 6.是否存在着相似名称的变量(如 VOLT 和 VOLTS)?这种情况不一定是错 误,但应被视为警告,这些名称可能会在程序中发生混淆
运算错误
1.是否存在不一致的数据类型(如非算术类型)的变量间的运算? 2.是否有混合模式的运算
3.是否有相同数据类型不同字长变量间的运算? 4.赋值语句的目标变量的数据类型是否小于右边表达式的数据类型或结果? 5.在表达式的运算中是否存在表达式向上或向下溢出的情况,也就是说,最终 的结果看起来是个有效值,但中间结果对于编程语言的数据类型可能过大或过小。 6.除法运算中的除数是否可能为 0? 7.如果计算机表达变量的基本方式是基于二进制的,那么运算结果是否不精
8. 在 特 定 场 合 , 变 量 的 值 是 否 超 出 了 有 意 义 的 范 围
9.对于包含一个以上操作符的表达式,赋值顺序和操作符的优先顺序是否正 确? 10.整数的运算是否有使用不当的情况,尤其是除法?
比较错误
1.是否有不同数据类型的变量之间的比较运算
2.是否有混合模式的比较运算,或不同长度的变量间的比较运算?如果有,应 确保程序能正确理解转换规则
3.比较运算符是否正确?程序员经常混淆“至多”、“至少”、“大于”、“不小于”、 “小于”和“等于”等比较关系。 4.每个布尔表达式所叙述的内容是否都正确
5.布尔运算符的操作数是否是布尔类型的?比较运算符和布尔运算符是否错 误地混住了一起
6.在二进制的计算机上,是否有用二进制表示的小数或浮点数的比较运算?
7.对于那些包含一个以上布尔运算符的表达式,赋值顺序以及运算符的优先顺 序是否正确?
8. 编 译 器 计 算 布 尔 表 达 式 的 方 式 是 否 会 对 程 序 产 生 影 响 ?
控制流程错误
接口错误
代码走查也是采用持续一至两个小时的小间断会议的形 式。代码走查小组由三至五人组成,其中一个人扮演类似代码检查过程中“协调人” 的角色,一个人担任秘书(负责记录所有查出的错误)的角色,还有一个人担任测 试人员。关于这二到五个人的组成结构,有各种各样的建议。当然,程序员应该是 其中之一。我们建议另外的参与者应该包括:(1)一位极富经验的程序员;(2)一位程 序设计语言专家;(3)一位程序员新手(可以给出新颖,不带偏见的观点),(4)最终 将维护程序的人员;(5)一位来自其他不同项目的人员;(6)一位来自该软件编程小组 的程序员。
提出的建议应针对程 序本身,而不应针对程序员。换句话说,软件中存在的错误不应被视为编写程序的 人员自身的弱点。相反,这些错误应被看作是伴随着软件开发的艰难性所固有的
因此桌面检查最好由其他人而非该程 序的编写人员来完成
大多数的软件项目都应使用到以下的人工测试方法
利用错误列表进行代码检查。 • 小组代码走查。 • 桌面检查。 • 同行评审。
我们推荐的步骤是先使用黑盒测试方法来设计测试用例,然后视情况需要使用 白盒测试方法来设计补充的测试用例。
第四章
白盒测试
白盒测试关注的是测试用例执行的程度或覆盖程序逻辑结构(源代码)的程度。
判定覆盖或分支覆盖是较强一些的逻辑覆盖准则。该准则要求必须编写足够的 测试用例,使得每一个判断都至少有一个为“真”和为“假”的输出结果。换句话 说 , 也 就 是 每 条 分 支 路 径 都 必 须 至 少 遍 历 一 次
如果每条分支路径都被执行到了,那么每 条语句也应该被执行到了。但是,仍然还有至少三种例外情况: • 程序中不存在判断。 • 程序或子程序/方法有着多重入口点。只有从程序的特定入口点进入时,某 条特定的语句才能执行到。
总的来说,对于包含每个判断只存在一种条件的程序,最简单的测试准则就是 设计出足够数量的测试用例,实现:(1)将每个判断的所有结果都至少执行一次; (2)将所有的程序入口(例如入口点或 ON 单元)都至少调用一次,以确保全部的 语句都至少执行一次。而对于包含多重条件判断的程序,最简单的测试准则是设计 出足够数量的测试用例,将每个判断的所有可能的条件结果的组合,以及所有的入 口点都至少执行一次(加入“可能”二字,是因为有些组合情况难以生成)。
等价划分(Equivalence Partitioning)
精心挑选的测试用例还应具备另 外两个特性: 1.严格控制测试用例的增加,减少为达到“合理测试”的某些既定日标而必 须设计的其他测试用例的数量。 2.它覆盖了大部分其他可能的测试用例。也就是说,它会告诉我们,使用或 不使用这个特定的输入集合,哪些错误会被发现,哪些会被遗漏
边界值分析
经验证明,考虑了边界条件的测试用例与其他没有考虑边界条件的测试用例相 比,具有更高的测试回报率。所谓边界条件,是指输入和输出等价类中那些恰好处于边界、或超过边界、或在边界以下的状态.边 界值分析方法和等价划分之间的重要区别是,边界值分析考察正处于等价划分边界 或在边界附近的状态
因果图有助于用一个系统的方法选择出高效的测试用例集。
因果 图实际上是一种数字逻辑电路(一个组合的逻辑网络)、但没有使用标准的电子学 符号,而是使用了稍微简单点的符号
生成测试用例时采用的过程如下
将规格说明分解为可执行的片段。这是必须的步骤,因为因果图不善于处 理较大的规格说明
确定规格说明中的因果关系。所谓“因”,是指一个明确的输入条件或输入 条件的等价类。所谓“果”,是指一个输出条件或系统转换(输入对程序或 系统状态的延续影响)。举例来说,如果某个事务引起文件或数据库记录被 修改,那么这种改变就是一个系统转换,而系统反馈的确认信息就是一个 输出条件。通过逐字逐句地阅读规格说明,同时标识出描述“因”和“果” 的文字或句子,就可以将“因”和“果”确定出来。因果关系一旦确定下 来,每个“因”和“果”都被赋予一个惟一的编号。 3.分析规格说明的语义内容,并将其转换为连接因果关系的布尔图。4.说明由于语法或环境的限制而不能联系起来的“因” 5.通过仔细地跟踪图中的状态变化情况,将因果图转换成一个有限项的判定 表。表中的每一列代表一个测试用例。 6.将判定表中的列转换成测试用例
如果测试用例的数 量大得不切合实际,就只得从中挑出一些子集来,而这又不能保证低效的测试用例 会被排除在外。因此,最好在分析因果图的阶段就将其排除掉
最后一个步骤,是将判定表转化为 38 个测试用
因果图方法是一个根据条件的组合而生成测试用例的系统性的方法。可以替代 这种方法的是特殊选取的条件组合,但在这个过程中,很可能会遗漏很多可由因果 图方法确定的“令人感兴趣的”测试用例。
一组合理的策略如下: 1.如果规格说明中包含输入条件组合的情况,应首先使用因果图分析方法。 2.在任何情况下都应使用边界值分析方法。应记住,这是对输入和输出边界 进行的分析。边界值分析可以产生一系列补充的测试条件,但是,也正如 “因果图分析”一节所述,多数甚至全部条件都可以被整合到因果图分析 中。 3.应为输入和输出确定有效和无效等价类,在必要情况下对上面确认的测试 用例进行补充。 4.使用错误猜测技术增加更多的测试用例。 5.针对上述测试用例集检查程序的逻辑结构
第五章 模块(单元)测试
模块测试的目的是将模块的功能与定义模块的功能规格说明或接口规格说明 进行比较
我们 从以下三个方面来探讨模块测试: 1.测试用例的设计方式。 2.模块测试及集成的顺序。 3.对执行模块测试的建议
在为模块测试设计测试用例时,需要使用两种类型的信息:模块的规格说明和 模块的源代码
模块测试的测试用例的设计过程如下:使用一种或多种白盒测试方法分析模块的逻 辑结构,然后使用黑盒测试方法对照模块的规格说明以补充测试用例
现在有两个观点清晰了:多重条件覆盖准则优先于其它准则;任何逻辑覆盖准 则尚不足以胜任作为生成模块测试用例的惟一手段。
在执行模块测试过程中,我们主要有两点考虑:第一,如何设计一个有效的测试 用例集;第二,将模块组装成工作程序的方式
软件测试是否应先独立地测试每个模块,然后再将这 些模块组装成完整的程序?还是先将下一步要测试的模块组装到测试完成的模块 集合中,然后再进行测试?第一种方法称为非增量测试或“崩溃(big-bang ) ”测 试,而第二种方法称为增量测试或集成
不同于独立地测试每个模块,增量测试首先 将下一个要测试的模块组装到前面已经测试过的模块集合中去
增量测试要优于非增量测试
按自顶向下模式设计的程序既可使用自顶向下的方式,也可使用自底向上的方式进 行增量测试。
自顶向下的测试是从程序的顶部或初始模块开始
挑选哪一个 后续模块进行增量测试没有惟一正确的方法:惟一的原则是:要成为合乎条件的下 一个模块,至少一个该模块的从属模块(调用它的模块)事先经过了测试。 我们用图 5-8 来说明这种测试策略。A 至 L 代表程序的 12 个模块。假定模块 J 包含程序的 I/O 读操作,而模块 I 包含 I/O 写操作。 第一步是测试模块 A,测试要求必须编写出代表 B、C 和 D 的桩模块。遗憾的 是,我们经常会错误理解桩模块的生成
实践中时常会发生的一个终极问题是,在进行到下一个模块前未能穷举测试此 模块。这来自于两个原因:一是由于将测试数据嵌入桩模块中存在困难,二是由于程序的较高层次通常会为较低层次提供资源。
自底向上的策略开始于程序中的终端模块
测试完这些模块之后,同样没有最佳的方法来挑选要进行增量测试的下一个模块。 惟一正确的原则是,要成为合乎条件的下一个模块,该模块所有的从属模块(它调 用的模块)都已经事先经过了测试。
每一模块都需要一个特殊的驱动模块:即包含着有效的测试输入、调用被测模块且 将输出显示出来(或将实际输出与预期输出作比较)的模块。
当测试用例造成模块输出的实际结果与预期结果不匹配的情况时,存在两个可 能的解释:要么该模块存在错误,要么预期的结果不正确
如果 发现某一部分模块存在大量错误,那么很有可能这些模块甚至包含着更多的错误, 只是尚未检查出来而已
软件产品开发周期的模型。 过程的流程可归结为以下 7 个步骤: 1.将软件最终用户的要求转换为一系列书面的需求。这些需求就是该软件产品要 实现的目标。 2.通过评估可行性与成本、消除相抵触的用户需求、建立优先级和平衡关系,将 用户需求转换为具体的目标。 3.将上述目标转换为一个准确的产品规格说明,将产品视为一个黑盒,仅考虑其 接口以及与最终用户的交互。该规格说明被称为“外部规格说明”。 4.如果该产品是一个系统,如操作系统、飞行控制系统、数据库管理系统或雇员 人事系统等,而不仅是一个程序
步骤就是系统设计。该步骤将系统分割为单独的程序、部件或子系统,并定 义它们的接口。 5.通过定义每个模块的功能、模块的层次结构以及模块间的接口,来设计程序或 程序集合的结构。 6.设计一份准确的规格说明,定义每个模块的接口与功能。 7.经过一个或更多的子步骤,将模块接口规格说明转换为每个模块的源代码算法
那么现在有三个补充的方法来预防或识别这 些错误。首先,我们可以使软件开发过程更加精密,以防其中出现很多错误;其次, 在每个阶段结束时可以引入一个独立的验证过程,在进入下一个阶段之前尽可能多 地发现问题
第三个方法是对不同的开发阶段采用不同的测试方法
举例来说: • 模块测试的目的是发现程序模块与其接口规格说明之间的不一致。 • 功能测试的目的是为了证明程序未能符合其外部规格说明。 • 系统测试的目的是为了证明软件产品与其初始目标不一致
第六章 功能测试
功能测试是一个试图发现程序与其外部规格说明之间存在不一 致的过程。
强度测试使程序承受高负载或强度的检验
诸如操作系统、数据库管理系统和远程处理系统等软件通常都有可恢复性目 标,说明系统如何从程序错误、硬件失效和数据错误中恢复过来
一个良好 的测试计划应包括: 1.目标。必须定义每个测试阶段的目标。 2.结束准则。必须制定准则以规定每个测试阶段何时可以结束,该问题将在 下一节中讨论。 3.进度。每个阶段都须有时间表。应指出何时设计、编写和执行测试用例, 某些软件技术,如极限编程(在本书第 8 章中讨论)要求在程序编码开始 之前就设计测试用例和单元测试
4.责任
5.测试用例库及标准。在大型项目中,用于确定、编写以及存储测试用例的 系统方法是必须的。
6.工具。必须确定需要使用的测试工具,包括计划由谁来开发或采购、如何 使用工具以及何时需要使用工具。
7..计算机时间
8.硬件配置
9.集成。
10.跟踪步骤
11.调试步骤
12.回归测试
判断 如何获得要发现的错误数量。得到这一数字需要进行下面几个预测: 1.预测出程序中错误的总数量。 2.预测这些错误中有多大比例可能通过测试而发现。 3.预测这些错误中有多少是由各个设计阶段产生的,以及在什么样的测试阶 段能够发现这些问题
如果我们计划进行 4 个月的功能测试、3 个月的系统测试,可以建立如下 3 个 结束准则
当发现并修改了 130 个错误之后(估计的 200 个编码和逻辑设计错误中的 65%),模块测试即告结束
当发现并修改了 240 个错误之后(200 个错误的 30%加上 300 个错误的 60 %),或功能测试进行了 4 个月之后,无论后面发生什么,功能测试即告结 束
当发现并修改了 111 个错误之后,或系统测试进行了 3 个月之后,无论以 后发生什么,系统测试即告结束。
第 七 章 调试(DEBUGGING)
调试是一个包含两个步骤的过 程,从执行了一个成功的测试用例、发现了一个问题之后开始。第一步,确定程序 中可疑错误的准确性质和位置;第二步,修改错误。
暴力调试方法可至少被划分为三种类型: 1.利用内存信息输出来调试。 2.根据一般的“在程序中插入打印语句”建议来调试。 3.使用自动化的调试工具进行调试。 第一种类型,使用内存信息输出(通常使用十六进制或八进制格式粗略地显示 所有的存储区域)是最缺乏效率的暴力调试方法,原因如下: • 难以在内存区域写源程序中的变量之间建立对应关系。 • 即使对下复杂程度较低的程序,内存信息输出也会产生数最非常庞大的数 据,其中的大多数都是与调试无关的。 • 内存信息输出显示的是程序的静态快照,仅能显示出在某一个时刻程序的 状态;为了发现错误,还需要研究程序的动态状态(随时间的状态变化)。 • 内存信息输出很少可以精确地在错误发生的地方产生,因此无法显示在错 误发生时程序的状态。错误发生到输出内存信息这段时间之内程序执行的 活动,可能会掩盖掉发现错误所需的线素。 • 通过分析输出的内存信息来发现问题的方法并不大多(因此很名程序员都 是密切注视,急切地渴望着错误能神奇地从内存信息输出中自行暴露出 来)。 第二种类型,在失效的程序中插入输出变量值的语句,这种做法也不具有很强 的优势。它可能比内存信息输出要好一些,因为可以显示程序的动态状态,让我们 检查的信息可以相对容易地与源程序联系起来。但是这种方法同样也有很多缺点: • 它不是鼓励我们去思考程序中的问题,而主要是一种碰运气的方法。
它所产生的需要分析的数据量非常庞大。 • 它要求我们修改程序,这些修改可能会掩盖掉错误、改变关键的时序关系, 或者会引入新的错误。 • 它可能对小型程序有效,但如果应用到大型程序,成本就相当高。况且对 于某些类型的程序,如操作系统或过程控制软件,这种办法甚至无法使用
归纳调试的步骤如下:
确定相关数据
组织数据
做出假设
证明假设
演绎的过程是从一些普遍的理论或前提出发,使用排除和精炼的过程,达到一 个结论
在小型程序中定位错误的一种有效方法是沿着程序的逻辑结构回溯不正确的 结果,直到找出程序逻辑出错的位置
调试的原则
定位错误的原则
如果遇到了僵局,就留到稍后解决
如果遇到了困境,就把问题描述给其他人听
仅将测试工具作为第二种手段
在试过了其他的方法之后才使用调试上具,并将其作为头脑思考的辅助手 段,而不是替代手段
避免使用试验法——仅将其作为最后的手段
修改错误的技术
存在一个缺陷的地方,很有可能还存在其他缺陷
应纠正错误本身,而不仅是其症状
正确纠正错误的可能性并非 100%
正确修改错误的可能性随着程序规模的增加而降低
应意识改正错误会引入新错误的可能性
修改错误的过程也是临时回到设计阶段的过程
应修改源代码,而不是目标代码
错误分析
详细的错误分析会包括如下内容
错误出现在什么地方?
谁制造了这个错误?
哪些做得不正确?
如何避免该错误的出现
为什么错误没有早些发现
该如何更早地发现错误
第八章 极限测试
XP 更倾向于适合中 小规模的软件开发,这些软件的规格说明的变更非常频繁,接近实时的沟通也是可 能的。
XP 实践可以归纳为 4 个概念: 1.聆听客户和其他程序员的谈话。 2.与客户合作,开发应用程序的规格说明和测试用例。 3.结对编码。 4.测试代码库
极限测试方法,该方法强调连续 测试
极限测试主要由两种类型的测试组成:单元测试和 验收测试
单元测试是极限测试中采用的主要测试方法,它具有两个简单规则:所有代码 模块在编码开始之前必须设计好单元测试用例,在产品发布之前须通过单元测试。
极限测试中的单元测试与前面描述的单 元测试之间的最大差别在于,极限测试中的单元测试必须在模块编码之前就完成设 计和生成