软件构造 6-5 Testing and Test First Programming

6.5 软件测试与测试优先的编程

一. 软件测试

1. 验证的种类

  为了发现程序中的问题,我们需要做一下工作,他们均属于验证

  • 形式推理:通过理论推导证明程序的正确性。而形式推理目前还缺乏自动化的工具,通常需要漫长的手工计算。但一些关键性的步骤也得证明,例如操作系统的调度程序、虚拟机里的字节码解释器,或者是文件系统。
  • 代码审查:即让别人仔细地阅读、审校、评价你的代码。
  • 测试:选择合适的输入输出样例,通过运行程序检查程序的问题。

2. 剩余缺陷率

  剩余缺陷率(residual defect rates):软件发行时存在的 bug 比率,对于每一千行代码(kloc):

  • 1 - 10 defects/kloc :常见的工业软件
  • 0.1 - 1 defects/kloc:高质量验证后的软件。如 Java 的官方库可能就是这个级别。
  • 0.01 - 0.1 defects/kloc:最好、最安全的如军工/安全关键软件。如 NASA 。

3. 软件测试的目的与困难之处

  测试是提高软件质量的重要手段,确认是否达到可用级别用户需求并关注系统的某一侧面的质量特性。
  作为一个测试者,你应该想方设法让程序崩溃
  测试是为了对正确性进行测试,输入都是规格说明(spec)的合法值


  然而再好的测试也无法证明系统里不存在错误。


  对于测试类型,我们无法在软件行业进行:

  • 尝试全部的可能:输入空间通常来说会非常大。
  • 随机测试:这通常难以发现 bug 。
  • 基于统计方法的测试:由于软件的行为通常是离散的且不可预测。故而这种测试方法并不好。简而言之,大多数正确,少数点出错;bug 出现往往不符合特定概率分布,或者说无统计分布规律可循。

4. 测试等级:

  • 单元测试:验证特定代码段的功能的测试,通常在功能级别。
  • 集成测试:由多个程序员或编程团队创建的两个或多个类、包、组件、子系统的联合执行。
  • 系统测试:对一个完全集成的系统进行测试,以验证该系统满足其要求,从而在其最终配置中执行软件。
  • 验收测试在这里插入图片描述
  • 回归测试图一
  • 其它测试

5. 静态测试与动态测试

  静态测试是在不实际执行程序的情况下执行的。
  静态测试通常是隐式的,例如校对以及当编程工具/文本编辑器检查源代码结构或编译器(预编译器)检查语法和数据流作为静态程序分析时。
  审查、演练或检查称为静态测试。


  动态测试动态测试描述了对代码的动态行为的测试,它实际上用给定的一组测试用例执行已编程代码。
  动态测试可以在程序100%完成之前开始,以便测试特定的代码段,并应用于离散的函数或模块。
  典型的技术是使用存根/驱动程序或从调试器环境执行。


  静态测试与动态测试最大的区别是:前者靠眼睛后者靠实际样例。

7. 测试与调试

  测试:发现是否存在错误。
  调试:识别错误根源,消除错误。

8.白盒测试与黑盒测试

  白盒测试:对程序内部代码结构的测试。
  黑盒测试:对程序外部表现出来的行为的测试。

二. 测试用例

  test case = {test inputs + execution conditions+ expected results} :
  测试用例:输入+执行条件+期望结果。

  • 测试用例是为特定的目标而开发的,例如执行特定的程序路径或验证特定需求的遵从性。
  • 测试用例可以是您向程序询问的一个问题。运行测试的目的是获取信息,例如程序是否通过测试。
  • 测试用例是质量保证的基石,而它们的开发是为了验证产品的质量和性能。

  良好测试用例的特征:

  • 最可能发现错误
  • 不重复、不冗余
  • 最有效
  • 既不简单又不复杂

  若干相关学术研究:

  • 根据 spec 和代码,自动生成测试用例
  • 测试用例的最小化
  • 回归测试的最小用例集

三.测试优先编程

  测试开始的时间应该尽量早,并且要频繁地测试。最好在写代码前写测试样例。


  在测试优先编程中,测试程序先于代码完成。编写一个函数应该按如下步骤进行:

  • 为函数写一个规格说明(spec)。
  • 为上一步的规格说明(spec)写一些测试用例。
  • 编写实际代码。一旦你的代码通过了所有你写的测试用例,这个函数就算完成了。

1. 规格说明

  规格说明描述了这个函数的输入输出行为。它确定了函数参数的类型和对它们的所有约束(例如 sqrt 函数的参数必须是非负的)、定义了函数的返回值类型以及返回值和输入之间的关系。在代码中,规格说明包括了函数签名一些描述函数功能的注释
  先完成测试用例的编写能够让你更好地理解规格说明。规格说明也可能存在问题——不正确、不完整、模棱两可、缺失边界情况。先尝试编写测试用例,可以在你浪费时间实现一个有问题的规格说明之前发现这些问题。

2. 通过等价类划分的方法选择测试用例

  测试空间足够小,以便能够快速完成测试;测试用例能够验证尽可能多的情况。

  为了达到这个目的,我们可以使用基于等价类划分的测试:将被测函数的输入域划分为等价类,从等价类导出测试用例。
  针对每个输入数据需要满足的约束条件划分等价类。每个等价类代表着对输入约束加以满足 / 违反的有效 / 无效数据的集合。

  分区背后的原理在于同一类型的数据在程序中的行为大多类似,所以我们可以用一小部分代表整体的行为。实质上是等价类的划分。这个方法的优点在于强迫程序响应输入空间里的不同地方,有效的利用了测试资源。

  如果我们要确保测试的输出能够覆盖输出空间的不同地方,也可以将输出空间划分为几个子域(哪些输出代表程序发生了相似的行为)。大多数情况下,对输入分区就足够了。


  由于程序员常犯丢失一个的错误,再加上边界处的值可能需要特殊的行为来处理,bug 经常会在各个分区的边界处发生,例如:

  • 在正整数和负整数之间的 0
  • 数字类型的最大值和最小值,例如 intdouble
  • 空集,例如空的字符串,空的列表,空的数组
  • 集合类型中的第一个元素或最后一个元素

  这时候需要使用边界值分析方法,这是对等价类划分方法的补充。


  覆盖分区时,我们选择尽力的程度来测试分区,其中有两个极限情况:

  • 完全笛卡尔乘积:对每一个存在的组合都进行测试。多个划分维度上的多个取值,要组合起来,每个组合都要有一个用例。然而要注意,有一些组合实际上不存在。这样做测试完备,但用例数量多,测试代价高
  • 每一个分区被覆盖即可:即每个等价类至少被覆盖一次。这基于假设:相似的输入,将会展示相似的行为。故可从每个等价类中选一个代表作为测试用例即可,从而可以降低测试用例数量。这样做测试用例少,代价低,但测试覆盖度未必高。

3. 单元测试

  一个良好的测试程序应该测试软件的每一个模块(方法或者)。如果这种测试每次是对一个孤立的模块单独进行的,那么这就称为“单元测试”。单元测试是针对软件的最小单元模型开展测试,隔离各个模块,容易定位错误调试

4. 用JUnit进行自动单元测试

  一个 JUnit 测试单元是以一个方法(method)写出的,其首部有一个 @Test 声明。一个测试单元通常含有对测试的模块进行的一次或多次调用,同时会用断言检查模块的返回值,比如 assertEquals, assertTrue, 和 assertFalse


  对于所有 JUnit 支持的断言,例如 assertEquals参数顺序很重要。它的第一个应该是我们期望的值,通常是一个我们算好的常数,第二个参数就是我们要进行的测试
  如果一个测试断言失败了,它会立即返回, JUnit 也会记录下这次测试的失败。一个测试类以有很多 @Test 方法,它们可以各自独立的进行测试,即使有一个失败了,其它的测试也会继续进行

5. spec 的书写

  我们应该在测试时记录下我们的测试策略,例如我们是如何分区的,有哪些特殊值、边界值等等;另外,每一个测试方法都要有一个小的注解,告诉读者这个测试方法是代表我们测试策略中的哪一部分。
   spec 的书写

6. 黑盒测试

  黑盒测试意味着只依据函数的规格说明来选择测试用例,而不关心函数是如何实现的。即黑盒测试:用于检查代码的功能,不关心内部实现细节。


  黑盒测试试图找到以下类型的错误:

  • 不正确或缺失的功能
  • 界面错误
  • 数据结构或外部数据库访问中的错误
  • 行为或性能错误
  • 初始化和终止错误

  这实际上就是检查程序是否符合规约。需要注意的是,用尽可能少的测试用例,尽快运行,并尽可能大的发现程序的错误。

6. 白盒测试

  白盒测试要考虑内部实现细节,即考虑函数的实际实现方法的前提下选择测试用例。如果代码实现中维护一个内部缓存记录之前得到的输入的答案,那你应该测试重复的输入


  测试用例不需要尝试规格说明没有明确要求实现行为。例如并没有指定抛出某类异常时,测试用例也应该“宽容地”对异常类型不做限制。


  白盒测试一般较早执行,其根据程序执行路径设计测试用例。

7. 代码覆盖度

  代码覆盖度已有的测试用例有多大程度覆盖了被测程序。通常使用百分比衡量。其中有三种常见的覆盖率:

  • 声明覆盖率:每个声明是否都被测试到了?在工业界,100%的声明覆盖率是普遍的要求,但这有时不可能实现,因为会存在一些“不可能到达的代码”(例如有一些断言)。
  • 分支覆盖率:对于每一个 ifwhile 等等控制操作,他们的分支是否都被测试过?100%的分支覆盖率是一种很高的要求,对于军工/安全关键的软件可能会有此要求。
  • 路径覆盖率:每一种分支的组合路径都被测试过?不幸的是,100%的路径覆盖率是不可能的,因为这会让测试用例空间以指数速度增长。

  测试效果、测试难度上均满足: 路 径 覆 盖 > 分 支 覆 盖 > 语 句 覆 盖 路径覆盖>分支覆盖>语句覆盖 >>


  一个标准的方法就是不断地增加测试用例直到覆盖率达到了预定的要求。在实践中,声明覆盖通常用覆盖率工具进行计数。利用这样的工具,白盒测试会变得很容易,你只需要不断地调整覆盖的地方,直到所有重要的声明都被覆盖到。
  在 Eclipse 中有一个好用的代码覆盖率工具 EclEmmaEclEmma 会将被执行过的代码用绿色标出,没有被执行的代码用红色标出。对于一个分支语句,如果它的一个分支一直没有被执行,那么这个分支判断语句会被标为黄色

8.集成测试和桩(stubs)

  与单元测试相反,集成测试是对于组合起来的模块进行测试,甚至是整个程序。如果集成测试报错,我们就只能在大的范围去找了。但是这种测试依然是必要的,因为程序经常由于模块之间的交互而产生 bug。由于单元测试是前置测试,搜索 bug 的范围将小很多。
  如果我们要做这样函数或方法的测试——函数或方法调用一个或多个其他孤立模块——我们将它调用的模块写成。特别地,一个的桩通常被称为“模拟对象”(mock object)。

9. 自动化测试和回归测试

  自动化测试(Automated testing)是指自动地运行测试对象,输入对应的测试用例,并记录结果的测试。
  能够进行自动化测试的代码称作测试驱动。一个测试驱动不应该在测试的时候停下来等待你的输入,而是自动调用模块输入测试用例进行测试,最后的结果应该是“测试完成,一切正常”或者“这些测试发了报错:…”。
  “自动化生成测试用例”是一个很难的问题,目前还处于活跃的研究之中。


  当你修改你的代码后,需要重新运行之前的自动化测试。我们称修改代码带来新的 bug 的现象为“回归”,而在修改后重新运行所有的测试称为“回归测试”。
  无论什么时候修改了一个 bug ,记得将导致 bug 的输入添加到你的测试用例里,并在以后的回归测试中去使用它——毕竟这个bug已经出现了,说明它可能是一个很容易犯的错误。
  在实践中,自动化测试和回归测试通常结合起来使用。因为回归测试只有自动化才可行(不然大量的测试没法实现)。反过来,如果你已经构建了自动化测试,你通常也会用它来防止回归的发生。所以自动化回归测试是软件工程里的一个“最佳实践”。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值