跳到主要内容

Ch 7. 带上 X 光眼镜测试软件

本章主要介绍动态白盒测试,解释调试和动态白盒测试的关系,以及单元测试、集成测试的有关概念,并讲述衡量测试完整性的各种方法。

动态白盒测试

动态白盒测试(又称结构化测试)是指利用查看代码功能(做什么)和实现方式(怎么做)得到的信息来确定需要测试的地方以及开展测试的方法。

由于白盒测试对软件的具体实现有了解,因此对于不同的实现方式就需要设计不同的测试用例。

示例

当你知道计算程序中一个是调用系统 API 计算,而另一个是靠将输入传输给服务端的计算员(老天,现实里不会有这种情况吧?)来计算并返回结果时,就需要有针对性地设计测试用例。因为二者的实现方式不同,可能存在缺陷的地方也不同。

动态白盒测试通常包含以下四个步骤:

  • 直接测试组件:测试组成软件的各个代码组件是否正常工作。
  • 测试完整程序:从顶层开始测试整个程序,根据对软件运行的了解程度调整测试用例。
  • 获取变量和状态信息:强制软件以正常测试难以实现的方式运行,获取运行时变量和状态信息的访问权,以便确定测试与预期结果是否一致。
  • 估算测试时覆盖的代码量:确定覆盖的代码量,并根据此调整测试用例,去掉多余的、补充遗漏的。

动态白盒测试与调试

调试(Debugging)是指在软件运行时发现问题并解决问题的过程。

动态白盒测试的目标是寻找软件缺陷,而调试的目标是解决软件缺陷。

提示

尽管目的不同,调试和动态白盒测试手段却十分相似。常用的代码级调试工具,如断点单步执行变量监视等,也可以用于动态白盒测试。

此外,比起程序员,测试员在执行这类白盒测试时通常会采用与生产环境不同的构建配置(称为调试构建测试构建),以便更好地获取运行时信息。

分段测试

分段测试的思想在于,如果代码能够分段构建并测试,最后将其合并在一起,那么整个产品出错的概率就会降低。

它的优势在于能够隔离不同层次的软件缺陷。先分别测试各个部分并找出独立工作时的问题,然后再将其合并在一起,这样再出现问题时就能知道大概率是连接各个组件的地方出了问题。

单元测试与集成测试

对每个原子组件进行的测试,称为单元测试(Unit Testing)或模块测试(Module Testing);对几个组件组合后的复合模块进行的测试则称为集成测试(Integration Testing)。最后,将整个软件——至少是主要部分——组合在一起进行测试,这就是系统测试(System Testing)。

包含中间的集成测试的测试称为增式集成,直接将所有组件组合在一起进行测试的测试称为非增式集成

增式集成测试的策略

增式集成有两种方式:自底向上(Bottom-Up)和自顶向下(Top-Down)。

应当根据底层和高层接口的变动频繁程度和开发预期计划,选择合适的集成策略。

自底向上

从最底层的模块开始编写和测试,组合成一个构件,用以完成指定的软件子功能。

自底向上方法通过称为测试驱动(Testing Driver)的模块来模拟上层模块的功能和协调本模块的输入输出。测试驱动向被测模块提供模拟输入,获取测试模块的输出并检验其是否符合预期。

测试结束后,用上层模块替换驱动程序,继续测试,直到所有模块都被测试。

优点在于能够尽早发现底层模块的问题,且生成测试数据的过程相对自由和简单;缺点在于需要编写驱动程序,且在测试末尾才能发现系统性或高层次的问题。

自顶向下

主控模块作为测试驱动,所有与主控模块直接相连的模块作为桩模块;根据集成的方式(深度或广度),每次用一个替换从属的桩模块;在每个模块被集成时,都必须已经进行了单元测试;进行回归测试(Regression Testing)以确定集成新模块后没有引入错误。

信息

回归测试指在修复旧的软件缺陷或发生代码变更后,重新运行之前的测试用例,以确保变更没有引入新的错误或缺陷。

相对地,自顶向下方法通过称为桩模块(Stub Module)的模块来模拟下层模块的功能。桩模块向被测模块提供模拟输出,测试驱动再根据被测模块的输出来检验其是否符合预期。

优点在于能够在一开始以高层次的视角看待软件,对测试的全局性和系统性问题有更好的把握;缺点在于需要编写桩模块,且难以预测底层模块的行为,底层测试不充分。

开展动态白盒测试

记住

在对软件开展白盒测试之前,必须确保已经经过了黑盒测试,或至少从模块的功能和作用的角度建立了相应的测试用例。

这是因为白盒测试容易陷入 从模块本身的工作方式思考 的陷阱,而忽略了软件的实际需求,最终导致即便从代码层面上完全测试了模块,软件也可能无法满足用户需求——就像下面这样:

啊,我知道了,这里代码写的是对的,我们不需要这个测试用例。

动态白盒测试是基于动态黑盒测试的测试用例的。可以根据软件的实现方式修改测试用例,以便更好地与代码一致。

示例

例如,也许在黑盒测试时将 a123123 当做不同的等价类,但发现实际上代码中将二者作为并列的进入某个分支的条件对待(if (input == "a123" || input == "123")),那么就可以将这两个等价类合并,以减少测试用例的数量。

数据覆盖

自然地,我们可以将我们在黑盒测试中的经验迁移到白盒测试中。通过将程序分为数据状态(或程序流程),我们可以从两个角度来检验我们的测试用例的有效情况。

首先考虑数据。数据广义上除了指常量变量、各种数据结构外,还指文件外部设备的输入输出

数据流覆盖

数据流覆盖(Data Flow Coverage)是指通过跟踪程序中的一批数据,检查其在各个组件间的流动情况与中间值,以确定测试用例的有效性。

次边界覆盖

在黑盒测试的 选择测试用例:次边界条件 中我们已经提到过这些隐性边界条件,而在白盒测试中由于我们获得了对代码的访问,需要更加深入地考虑它们。

错误强制

如果不能通过合法方式输入含有错误数据的测试用例,就需要借助调试器或其他手段来强制赋值,这被称为错误强制(Error Forcing)。

错误强制的目的是检验软件在异常情况下的表现,要求软件对错误进行处理并继续运行。

提示

错误强制并不是去故意制造错误(例如用现实中不可能的输入),而是模拟可能的异常情况,以检验软件的容错性。

代码覆盖

为了全面地测试软件,我们需要考虑代码覆盖(Code Coverage)。

通常使用代码覆盖率测试器来记录程序中各个分支和函数的调用情况。代码覆盖率测试器会生成一个代码覆盖率报告,告诉我们哪些测试用例是重复的、哪些是遗漏的,此外还能反映出软件质量的大致情况。

记住

不要忘记 杀虫剂怪事

覆盖率很高却没有发现任何问题的情况应该引起我们的警惕。倘若 90% 的代码都被覆盖而不存在任何缺陷,那么在余下的 10% 代码中就可能存在着相当多的缺陷。

语句覆盖与代码行覆盖

语句覆盖(Statement Coverage)或代码行覆盖(Line Coverage)是指测试过程中覆盖的语句或代码行占总代码行数的比例。

然而,语句覆盖实际具有相当的误导性——它并不能保证所有的分支都被覆盖。因此,我们还需要考虑其他的覆盖率指标。

路径覆盖

路径覆盖(Path Coverage)是指测试过程中覆盖的程序路径占总程序路径的比例。

分支覆盖

这是路径覆盖最简单的形式。它要求每个分支(例如 if-else 语句等)至少被执行一次。

要注意的是,隐性分支(例如不带 elseif 语句)也需要被覆盖。

条件覆盖

这是分支覆盖的一种扩展。它要求每个条件语句对应的布尔表达式的所有可能取值至少被覆盖一次。

如果达成条件覆盖,那么分支覆盖也一定被满足,同时语句覆盖同样达成。


小测验

问题摘录与参考回答

为什么了解了软件的工作方法会影响测试的方式和内容?

答:

因为软件的实现方式不同,可能存在不同的缺陷。

此外,白盒测试提供了对代码的访问,可以从实现和覆盖率的角度检验黑盒测试用例的有效性。

判断是非:如果匆忙开发产品,就可以跳过模块测试而直接进行集成测试。

答:

错误,这样会忽视本该在早期发现的软件缺陷。

测试桩和测试驱动有什么区别?

答:

测试桩在自顶向下测试中使用,用于模拟底层模块;测试驱动则相反,在自底向上测试中用于模拟上层模块。

判断是非:总应该先设计黑盒测试用例。

答:

正确。测试用例的设计总是基于需求和对软件行为的认识程度,基于代码的设计只是为了使其更加有效。