根据这一定义,即便你能执行一场绝对完美的模块测试,也依然无法保证已经找出了所有的软件错误。因此,要完成完整的测试,还必须进行某种形式的进一步测试,我们将这种新的测试形式称为高阶测试。
软件开发在很大程度上是一个传递最终程序相关信息,并将这些信息从一种形式转换为另一种形式的过程。本质上,它是从概念到具体实现的转化过程。正因如此,绝大多数软件错误都可以归因于信息传递与转换过程中出现的中断、失误和 “干扰”。
图 6.1 展示了这种软件开发观点,它是软件产品的开发周期模型。整个流程可以概括为七个步骤:
将程序用户的需求转化为书面的需求规格说明,这是产品的目标。
通过评估可行性、时间与成本,解决冲突需求,确定优先级与权衡方案,将需求转化为具体的目标。
将目标转化为精确的产品规格说明,将产品视为黑盒,只关注它与最终用户的接口和交互。这份描述称为外部规格说明。
如果产品是系统(如操作系统、飞行控制系统、数据库系统、人事管理系统)而非应用程序(如编译器、工资程序、文字处理器),下一步就是系统设计。这一步将系统划分为独立的程序、组件或子系统,并定义它们之间的接口。
设计程序的结构:明确每个模块的功能、模块的层次结构以及模块间的接口。
制定精确的规格说明,定义每个模块的接口与功能。
通过一个或多个子步骤,将模块接口规格说明转化为每个模块的源代码与算法。
我们可以从另一个角度理解这些文档:
需求说明为什么需要这个程序。
目标说明程序应该做什么,以及做到什么程度。
外部规格说明定义程序对用户的精确呈现方式。
后续过程的相关文档则以越来越细的粒度说明程序如何构建。
基于开发周期的七个步骤都涉及信息的沟通、理解与转换,且大多数软件错误源于信息处理失败这两个前提,我们可以采用三种互补的方法来预防和 / 或发现这些错误。
第一,在开发过程中引入更高的精确性,从源头预防大量错误。第二,在每个过程结束时增加一个独立的验证步骤,在进入下一阶段前尽可能多地发现错误。这一方法如图 6.2 所示。例如,通过将外部规格说明与上一阶段的输出(目标说明)进行比对来验证外部规格,并将发现的任何错误反馈到外部规格编写过程中。(第七步结束时的验证步骤可使用第 3 章讨论的代码审查和走查方法。)
第三种方法是让不同的测试过程对应不同的开发过程。也就是说,让每一种测试专注于某一个特定的转换步骤 —— 从而专注于某一类特定的错误。这一方法如图 6.3 所示。
测试周期的结构是对开发周期的建模。换句话说,你应该能在开发过程与测试过程之间建立一一对应关系。例如:
模块测试的目的是发现程序模块与其接口规格之间的不一致。
功能测试的目的是证明程序与外部规格不匹配。
系统测试的目的是证明产品与最初目标不一致。
注意我们是如何表述这些目的的:“发现不一致”“不匹配”“不一致”。请记住,软件测试的目标是发现问题(因为我们知道问题必然存在!)。如果你试图证明某些输入能正常工作,或假设程序符合规格与目标,那么你的测试就是不完整的。只有抱着证明某些输入会出错、假定程序不符合规格与目标的态度去测试,测试才是完整的。这是本书反复强调的重要概念。
这种结构的优点在于:它能避免无效的重复测试,并防止你遗漏大类错误。例如,系统测试不再只是简单地被称为 “对整个系统的测试” 并可能重复之前的测试,而是专门针对一类特定错误(即把目标转化为外部规格过程中产生的错误),并依据开发过程中某一类特定文档来衡量。
图 6.3 所示的高阶测试方法最适用于软件产品(按合同开发或面向广泛使用的程序,而非实验性程序或仅供作者自用的程序)。非产品化的程序通常没有正式的需求与目标,对这类程序而言,功能测试可能是唯一的高阶测试。此外,对高阶测试的需求会随程序规模增大而增强。原因在于,大型程序中设计错误(早期开发阶段产生的错误)与编码错误的比例远高于小型程序。
注意,图 6.3 中的测试过程顺序不一定代表时间顺序。例如,系统测试并非定义为 “功能测试之后做的测试”,而是专注于特定类别错误的独立测试,因此它完全可以与其他测试过程部分时间重叠。
本章将讨论功能测试、系统测试、验收测试和安装测试过程。我们省略了集成测试,因为它通常不被视为独立的测试步骤;而且在使用增量式模块测试时,它已隐含在模块测试中。
我们将对这些测试过程的讨论保持简洁、概括,且大多不使用示例,因为这些高阶测试中使用的具体技术高度依赖于被测试的特定程序。例如,操作系统的系统测试特性(测试用例类型、设计方式、使用的测试工具)与编译器、核反应堆控制程序或数据库应用程序的系统测试会有显著差异。
本章最后几节将讨论测试的规划与组织问题,以及一个关键问题:如何确定何时停止测试。
功能测试
如图 6.3 所示,功能测试是试图找出程序与外部规格之间不一致的过程。外部规格是从最终用户视角对程序行为的精确描述。
除小型程序外,功能测试通常是黑盒测试活动。也就是说,依靠之前的模块测试来满足所需的白盒逻辑覆盖标准。
执行功能测试时,你需要分析规格说明,推导出一组测试用例。第 4 章介绍的等价类划分、边界值分析、因果图、错误推测等方法尤其适用于功能测试。事实上,第 4 章的示例就是功能测试的示例。Fortran 的 DIMENSION 语句、考试评分程序、DISPLAY 命令的描述实际上都是外部规格的示例。不过,它们并非完全现实的示例;例如,评分程序的真实外部规格会包含报表格式的精确描述。(注:由于第 4 章已讨论功能测试,本节不再给出功能测试示例。)
我们在第 2 章提供的许多指导原则也特别适用于功能测试。尤其要记录出现错误最多的功能;这些信息很有价值,因为它表明这些功能很可能还存在大量尚未发现的错误。同时,要对无效和非预期的输入条件给予足够关注。(请记住,预期结果的定义是测试用例的关键部分。)
最后,一如既往,请牢记:功能测试的目的是暴露错误和与规格的不一致,而不是证明程序符合外部规格。
系统测试
系统测试是最容易被误解、也是难度最大的测试过程。系统测试不是对完整系统或程序功能的测试,因为这会与功能测试重复。相反,如图 6.3 所示,系统测试有一个明确的目的:将系统或程序与其最初目标进行比对。基于这一目的,可以得出两点推论:
系统测试不局限于 “系统”。如果产品是一个程序,系统测试就是试图证明程序整体未能满足其目标的过程。
根据定义,如果产品没有书面、可量化的目标,系统测试就是不可能的。
在寻找程序与目标之间的不一致时,重点应放在将目标转化为外部规格过程中产生的转换错误上。这使得系统测试成为至关重要的测试过程,因为就产品而言,开发周期中的这一步通常最容易出错,且错误数量最多、影响最严重。
这也意味着,与功能测试不同,外部规格不能作为系统测试用例的依据,因为这会违背系统测试的目的。另一方面,仅靠目标文档也无法设计测试用例,因为根据定义,它不包含程序外部接口的精确描述。我们的解决办法是同时使用程序的用户文档:
通过分析目标来设计系统测试思路;
通过分析用户文档来编写具体测试用例。
这样做还有一个附带效果:可以同时比对程序与目标、程序与用户文档、以及用户文档与目标之间的一致性,如图 6.4 所示。
图 6.4 也说明了为什么系统测试是难度最大的测试过程。图中最左侧的箭头(比对程序与目标)是系统测试的核心目的,但目前并没有成熟的测试用例设计方法学。原因在于,目标说明的是程序应该做什么、做到什么程度,但不说明功能的呈现形式。
因此,这里采用一种不同的测试用例设计思路:不描述方法学,而是列出系统测试用例的明确类别。由于缺乏方法学,系统测试需要大量创造力;事实上,设计优秀的系统测试用例所需要的创造力、智力和经验,甚至超过设计系统或程序本身。
表 6.1 列出了 15 类测试用例及其简要说明:
功能测试:确保目标中定义的功能已实现。
容量测试:让程序处理异常大量的数据。
压力测试:让程序承受异常高的负载,通常是并发处理。
易用性测试:评估最终用户与程序交互的友好程度。
安全性测试:尝试突破程序的安全机制。
性能测试:检查程序是否满足响应时间与吞吐量要求。
存储测试:验证程序是否正确管理系统与物理存储。
配置测试:在推荐配置上验证程序运行情况。
兼容性 / 转换测试:检查新版本是否兼容旧版本,数据转换是否正常。
安装测试:确保安装程序在所有支持平台上可用。
可靠性测试:验证程序是否满足可靠性指标(如 uptime、平均无故障时间 MTBF)。
恢复测试:测试系统的故障恢复机制是否按设计工作。
可维护性 / 可服务性测试:检查程序是否提供支持与维护所需的信息与机制。
文档测试:验证所有用户文档的准确性。
流程测试:验证使用或维护程序所需的人工流程是否正确。
我们不要求这 15 类适用于所有程序,但为避免遗漏,建议设计测试用例时逐一检查。
功能验证测试
最直观的系统测试,是检查目标中提到的每一项能力 / 功能是否真正实现。做法是逐句扫描目标,当某句描述 “应具备什么能力” 时,验证程序是否满足。这类测试有时甚至不需要上机,只需在头脑中将目标与用户文档比对即可。
容量测试
让程序处理极大数据量。例如:给编译器一个超大源程序;给链接编辑器一个包含数千个模块的程序;让操作系统的作业队列填满。目的是证明程序无法处理目标中规定的数据量。
压力测试
压力测试是让程序承受高强度负载 / 压力。不要与容量测试混淆:压力是短时间内的峰值活动量。
例如:
空中交通管制系统模拟同时出现远超设计数量的飞机;
操作系统同时运行最大数量的并发任务;
网站承受大量用户同时访问;
手机系统同时启动多个应用并接打电话。
即便某些压力场景在现实中 “几乎不会出现”,只要能发现错误,这类测试依然有价值 —— 因为同样的错误很可能在更温和的真实场景中出现。
易用性测试
易用性(用户测试)如今愈发重要。让最终用户在真实环境中测试软件,往往能发现自动化测试难以发现的问题。
安全性测试
针对程序的安全目标,设计用例尝试突破安全检查。例如绕过内存保护、破解数据访问权限、研究同类系统的已知漏洞并复现。
性能测试
许多程序有明确的性能目标(响应时间、吞吐量)。系统测试的目的是证明程序不满足性能目标。
存储测试
设计用例证明程序不满足存储目标,如内存占用、临时文件大小、磁盘空间管理等。
配置测试
在最小配置、最大配置、推荐配置、不同操作系统、不同浏览器上测试。
兼容性 / 转换测试
测试新版本是否兼容旧数据、旧格式、旧系统,数据迁移与转换是否正确。
安装测试
安装过程是用户的第一体验。安装失败会直接导致用户放弃产品。必须测试所有支持平台的安装流程。
可靠性测试
如果目标中有可靠性指标(如可用性 99.97%、MTBF 平均无故障时间),需要进行相应测试。
恢复测试
故意注入程序错误、模拟硬件故障、数据错误,验证系统能否正确恢复,是否满足平均恢复时间 MTTR 目标。
可维护性 / 可服务性测试
测试诊断工具、调试流程、日志、内部文档是否满足维护目标。
文档测试
以用户文档为依据编写测试用例,验证文档示例是否正确、描述是否准确。
流程测试
测试操作员、管理员、用户需要执行的人工流程是否清晰、可行、正确。
如何执行系统测试
实施系统测试最关键的问题之一是:由谁来做。
简单说:
程序员不应该做系统测试;
开发团队不应该做系统测试。
原因:
系统测试需要站在最终用户的角度思考;
开发人员对自己的产品有心理倾向,难以做到 “破坏性” 测试;
开发团队的目标是顺利推进项目、按时完成,而不是拼命证明产品不满足目标。
理想的系统测试团队应由:
专业系统测试专家
代表性最终用户
人机交互工程师
早期分析师 / 设计师(不直接编码)
组成。最经济高效的方式往往是:外包给独立测试机构。
验收测试
如图 6.3,验收测试是将程序与初始需求和当前用户实际需要进行比对的过程。它通常由客户 / 最终用户执行,不属于开发团队的责任。
定制项目:用户按合同验收;
产品软件:客户先测试是否满足自身需求。
验收测试的正确思路依然是:设计用例证明程序不满足需求。若无法证明,则可接受。
安装测试
安装测试的目的不是找软件本身的错误,而是找出安装过程中出现的错误。
需要测试:
选项选择是否合理
文件、库是否正确创建与加载
硬件配置是否合法
网络依赖是否满足
安装测试用例应由开发方编写,随产品一起交付,在安装完成后运行。
测试规划与控制
大型系统的测试可能涉及:数万测试用例、上千模块、数千错误修复、数百人、持续一年以上。因此测试管理极其重要。
一份好的测试计划应包含:
各阶段测试目标
完成标准
进度与时间表
职责划分
测试用例库与规范
测试工具
计算机资源
硬件配置
集成策略
跟踪机制
调试与错误报告流程
回归测试计划
测试完成标准
测试中最难回答的问题是:什么时候可以停止?
实践中最常见、但毫无意义甚至有害的停止标准:
到时间就停
所有用例跑过没报错就停
这两种都不衡量测试质量,还会鼓励设计 “低发现率” 的温和用例。
更有用的三类标准:
- 基于方法学的标准
要求必须使用某种测试方法(如边界值、因果图、多条件覆盖),且所有用例执行通过。优点:规范;缺点:主观、难以衡量、不适用于系统测试。 - 基于错误数量的标准(最有价值)
测试目标是发现错误,那么完成标准就可以是:发现并修复预定数量的错误。
需要三个估算:
程序中总错误数估计
测试能发现的比例
各类错误在各测试阶段的分布
例如:
模块测试:发现 65% 的编码错误再结束
功能测试:发现预定错误数 或 到期,取较晚者
系统测试:同理
如果程序太好、错误太少,达不到预定数量,可由外部专家评估用例质量,判断是用例不足还是程序真的很稳定。 - 基于错误发现率曲线
绘制单位时间发现错误数量的曲线:
曲线仍高位:继续测
曲线明显下降并趋于平缓:可停止,进入下一阶段测试
最佳实践是三者结合:
模块测试:用方法学标准
功能 / 系统测试:用错误数量 + 时间 + 趋势曲线综合判断
独立测试机构
我们反复强调:开发组织不应测试自己的产品。
测试组织应在组织结构上尽可能独立于开发,最好是完全独立的外部公司。优点:
动机更纯粹:目标就是找错
与开发形成健康制衡
不受开发管理层压力影响
具备专业测试知识与经验
总结
高阶测试可以理解为模块测试之后的整体测试。
功能测试:对照外部规格找错
系统测试:对照最初目标找错
系统测试是开发周期中最容易引入严重错误的阶段,因此也最重要、最难。
系统测试可围绕 15 个类别展开,需要极强的创造力。对大型系统而言,严格、一致的测试规划是成功关键,可考虑引入独立测试机构。
第 7 章我们将进一步展开高阶测试中的一个重要方面:用户测试 / 易用性测试。
浙公网安备 33010602011771号