软件构造知识点总结(一)静态检查与测试

软件构造知识点总结(一)静态检查与测试

1.自动检查

编程语言通常能提供以下三种自动检查的方法:

  • 静态检查: bug在程序运行前发现

  • 动态检查: bug在程序运行中发现

  • 无检查: 编程语言本身不帮助你发现错误,你必须通过特定的条件(例如输出的结果)检查代码的正确性。

很明显,静态检查好于动态检查好于不检查。

1.1静态检查

一般可以发现如下错误:

  • 语法错误,例如多余的标点符号或者错误的关键词。(动态类型的语言例如Python中也会做这种检查:如果你有一个多余的缩进,在运行之前就能发现它)。

  • 错误的名字,例如函数名称、类名等拼写错误。

  • 参数的个数不对

  • 参数的类型不对

  • 错误的返回类型

1.2动态检查

  • 非法的变量值,例如位于分母变量为0。
  • 无法表示的返回值。例如最后得到的返回值无法用声明的类型来表示。
  • 越界访问。例如在一个字符串中使用一个负数索引。
  • 引用一个null对象

静态检查倾向于类型错误 ,即与特定的值无关的错误。正如上面提到过的,一个类型是一系列值的集合,而静态类型就是保证变量的值在这个集合中,但是在运行前我们可能不会知道这个值的结果到底是多少。所以如果一个错误必须要特定的值来“触发”(例如除零错误和越界访问),编译器是不会在编译的时候报错的。与此相对的,动态类型检查倾向于特定值才会触发的错误。

2.静态类型、动态类型

Java是一种静态类型的语言。所有变量的类型在编译的时候就已经知道了(程序还没有运行),所以编译器也可以推测出每一个表达式的类型。

动态类型语言中(例如Python),这种类型检查是发生在程序运行的时候

静态类型是静态检查的一种(检查发生在编译的时候)

3.测试

3.1为什么软件测试很困难?

  • 全部测试(尝试所有的可能):这通常是不可行的,因为大多数情况下输入空间会非常大
  • 随机测试 : 这通常难以发现bug,除非这个程序到处都是bug以至于随便一个输入都能崩溃。即使我们修复了测试出来的bug,随机的输入也不能使我们对程序的正确性很确定。
  • 基于统计方法的测试:遗憾的是,这种方法对软件不那么奏效。在物理系统里,工程师可以通过特定的方法加速实验的进程,例如在一天的时间里打开关闭一个冰箱门一千次,以此来模拟几年的正常使用,最终得到产品的”失败率“。以后的测试结果也将会集中分布在这个比率左右,工程师们就对这个比率进行进一步的研究。但是软件的行为通常是离散且不可预测的。程序可能在上一秒还完全正常的工作,突然就崩溃了,也可能对于大多数输入都没问题,对于一个值就崩溃了。

3.2测试优先编程

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

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

规格说明描述了这个函数的输入输出行为。它确定了函数参数的类型和对它们的所有约束(例如sqrt函数的参数必须是非负的)。它还定义了函数的返回值类型以及返回值和输入之间的关系。

先完成测试用例的编写能够让你更好地理解规格说明。规格说明也可能存在问题:不正确、不完整、模棱两可、缺失边界情况。先尝试编写测试用例,可以在你浪费时间实现一个有问题的规格说明之前发现这些问题。

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

选择合适的测试用例是一个具有挑战性但是有缺的问题。我们即希望测试空间足够小,以便能够快速完成测试,又希望测试用例能够验证尽可能多的情况。

为了达到这个目的,我们可以依据等价关系先将输入空间划分为几个等价类 ,每一个等价类 都是一类相似的数据。我们在每个子域中选取一些数据,它们合并起来就是我们需要的输入用例。

举个简单的例子:

public int mutiply(int a,int b)
{
    return a*b;
}

对于这个函数,输入的参数有两个,一般可以考虑如下划分:

  • a和b都是正整数
  • a和b都是负整数
  • a是正整数,b是负整数
  • b是正整数,a是负整数

这里也有一些特殊的情况要单独分出来:0 1 -1

  • a或b是1,0,-1
  • a或b较小
  • a或b的绝对值大于Long.MAX_VALUE

所以我们一共可以得到 7 × 7 = 49 个分区,它们完全覆盖了a和b组成的所有输入空间:

在这里插入图片描述

依据划分的等价类,一般测试用例的选取有两种方案:

  • 完全笛卡尔乘积
    即对每一个存在组合都进行测试。例如上例,我们可以选取 7 × 7 = 49 个测试用例,每一个组合都用上了。
  • 每一个分区被覆盖即可
    因为一些测试用例可能覆盖了好几类等价类,所以该方法只要求每一个分区至少被覆盖一次

3.4注意等价类之间的“边界”

bug经常会在各个等价类的边界处发生,例如:

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

所以,我们在划分完毕后后,测试用例不要忘了加上边界上的值。

4.黑盒测试与白盒测试

黑盒测试意味着只依据函数的规格说明来选择测试用例,而不关心函数是如何实现的。

白盒测试 的意思是在考虑函数的实际实现方法的前提下选择测试用例。比如说,如果函数的实现中,对不同的输入采用不同的算法,那么你应该根据这些不同的区域来分类。

在做白盒测试时。你必须注意:你的测试用例不需要尝试规格说明中没有明确要求的实现行为。例如,如果规格说明中说“如果输入没有格式化,那么将抛出异常”,那么你不应该特地的检查程序是否抛出NullPointerExpection异常,因为当前的代码实现决定了程序有可能抛出这个异常。在这种情况下,规格说明允许任何异常被抛出,所以你的测试用例同样应该“宽容”地保留实现者的自由。

5.测试覆盖率

一种判断测试的好坏的方法就是看该测试对软件的测试程度。这种测试程度也称为“覆盖率”。以下是常见的三种覆盖率:

  • 声明覆盖率: 每一个声明都被测试到了吗?
  • 分支覆盖率:对于每一个ifwhile 等等控制操作,它们的分支都被测试过吗?
  • 路径覆盖率: 每一种分支的组合路径都被测试过吗?

其中,分支覆盖率要比声明覆盖率严格(需要更多的测试),路径覆盖率要比分支覆盖率严格。在工业界,100%的声明覆盖率一个普遍的要求,但是这有时也是不可能实现的,因为会存在一些“不可能到达的代码”(例如有一些断言)。100%的分支覆盖率是一种很高的要求,对于军工/安全关键的软件可能会有此要求。不幸的是,100%的路径覆盖率是不可能的,因为这会让测试用例空间以指数速度增长。

一个标准的方法就是不断地增加测试用例直到覆盖率达到了预定的要求。在实践中,声明覆盖通常用覆盖率工具进行计数。利用这样的工具,白盒测试会变得很容易,你只需要不断地调整覆盖的地方,直到所有重要的声明都被覆盖到。

6.单元测试、回归测试、集成测试

6.1单元测试

对孤立的模块进行测试。这使得debugging变得简单,当一个单元测试报错是,我们只需要在这个单元找bug,而不是在整个程序去找

6.2集成测试

与单元测试相对应的,集成测试是对于组合起来的模块进行测试,甚至是整个程序。如果集成测试报错,我们就只能在大的范围去找了。但是这种测试依然是必要的,因为程序经常由于模块之间的交互而产生bug。例如,一个模块的输入是另一个模块的输出,但是设计者在设计模块的时候将输入输出类型弄错了。另外,如果我们已经做好单元测试了,即我们可以确性各个单元独立的正确性,我们的搜索bug的范围也会小很多。

6.3回归测试

我们称修改代码带来新的bug的现象为“回归”,而在修改后重新运行所有的测试称为“回归测试”。

一个好的测试应该是能发现bug的,你应该不断的充实你的测试用例。所以无论什么时候修改了一个bug,记得将导致bug的输入添加到你的测试用例里,并在以后的回归测试中去使用它——毕竟这个bug已经出现了,说明它可能是一个很容易犯的错误。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值