软件测试的总结(1)

本文是对MIT讲义Reading 3: Testing翻译的第一部分,第二部分请见软件测试的总结(2)


本文的翻译主要用于我自己的学习,不追求绝对的准确性,但欢迎读者质疑

为什么软件测试是困难的

  • 彻底的测试是不可行的
  • 简单的运行一下不太可能找到bug,除非程序的bug非常多以至于随意的运行都有bug出现
  • 随机抽样测试或者统计测试对软件测试来说不是非常的有效,因为bug的出现可能与概率无关,并且不是连续分布的

优先测试编程 test-first-programming

  • 尽早并经常进行测试。不要把测试放在开发程序的后面,那样的话会造成debug的过程更长也更痛苦,因为bug可能存在于你的程序的任何地方。边开发边测试是一种更好的方式。
  • 优先测试编程要求你编写代码之前要先编写测试,单一函数的开发按着如下的顺序进行:
    1. 给这个函数写一个规范
    2. 编写符合规范的测试
    3. 编写实际的代码。当你的代码通过了所有的测试,你就成功了。
  • 规范描述了这个函数输入和输出的行为,他约定了参数的类型和对参数的其他约束(例如sqrt的参数必须是非负的),它也给定了返回值的类型以及返回值与输入的关系。

按分区选择测试用例

  • 将输入空间分成若干子域,每个子域由一组输入组成,所有的子域就组成了输入空间
  • 我们从每一个子域中选择一个测试用例,这就是test suite.
  • 子域背后的想法是将输入空间划分成一组类似的输入,这些输入是程序就有类似的行为。然后我们使用每个输入集合的一个来代表整个集合,这种方法通过选择不相似的测试用例来最大限度的利用有限的测试资源,并迫使测试探索随机测试可能无法达到的输入空间部分。

Example: BigInteger.multiply()

multiply的声明如下:

1
2
3
4
5
/**
* @param val another BigIntger
* @return a BigInteger whose value is (this * val).
*/
public BigInteger multiply(BigInteger val)

让我们拿一个例子看一下它怎样使用:

1
2
3
BigInteger a = ...;
BigInteger b = ...;
BigInteger ab = a.multiply(b);

这个例子表明,即使在方法的声明中只显示了一个参数,multiply实际上也是两个参数的函数:你调用方法的对象(上面的例子中的a)以及你传递给括号的参数(上面的例子中的b)。

所以我们有一个二维输入空间,由所有的整数对(a,b)组成。现在我们来分割它。考虑乘法是如何工作的,我们划定如下的分区

  • 0
  • 1
  • -1
  • small positive integer
  • small negative integer
  • huge positive integer
  • huge negative integer

这样就形成了如下如所示的7 × 7 = 49个分区
multiply-partition
为了生成测试套件,我们将从网格的每个方块中选取一对任意对(a,b),比如:

  • Example
    • (a,b) = (-3,25) to cover (small negative, small positive)
    • (a,b) = (0,30) to cover (0, small negative, small positive)
    • etc.

Example: max()

让我们来看Java library中的另一个例子:Math类中的max()函数

1
2
3
4
5
6
/**
* @param a an argument
* @param b another argument
* @return the larger of a and b.
*/
public static int max(int a, int b)

在数学角度上,这个方法是下列类型的函数:
max : int x int -> int
从规范上,应该将其划分为:

  • a < b
  • a = b
  • a > b
    所以我们的test suite 应该是:

  • (a, b) = (1, 2) to cover a < b

  • (a, b)= (9, 9) to cover a = b
  • (a, b) = (-5, -6) to cover a > b

分区如下所示:
max partition

在分区中包含边界

bug常常由不同分区的边界引起,比如:

  • 0是正数和负数的边界
  • int型的最大值和最小值
    为什么bug会经常出现在边界呢?,有这样几个原因:

  • 程序员经常犯off-by-one错误(像将 <= 误写成 <,或者将本应该初始为0的counter初始为1)

  • 一些边界需要被当做特殊情况处理
  • 边界可能是代码行为的不连续点

将边界作为子分区包含在分区中很重要,以便您从边界选择输入
让我们划分一下max : int x int -> int

  • relationship between a and b
    • a < b
    • a = b
    • a > b
  • value of a
    • a = 0
    • a > 0
    • a < 0
    • a = minimum integer
    • a = maximum integer
  • value of b
    • b = 0
    • b > 0
    • b < 0
    • b = minimum integer
    • b = maximum integer

现在让我们挑选一些覆盖了所有这些分区的测试值:

  • (1, 2) covers a < b, a > 0, b > 0
  • (-1, -3) covers a > b, a < 0, b < 0
  • (0, 0) covers a = b, a = 0, b = 0
  • (Integer.MIN-VALUE, Integer.MAX_VALUE) covers a < b, a = minint, b = maxint
  • (integer.MAX_VALUE, Integer.MIN_VALUE) covers a > b, a = maxint, b = minint

覆盖分区的两种极端

当我们对输入空间进行分区之后,我们就可以选择test suite 的详细程度

  • 每个分区都取出一个测试用例
    就像我们在示例multiply中所做的,我们取出了所有可能的49中情况

  • 覆盖所有的部分就可以
    就行我们在示例max中所做的,我们并没有所有的取出3 x 5 x 5 = 75中组合,而是精心的取出了5个测试用例,这5个测试用例包含了所有的情况

我们常常根据人工的判断和谨慎在这两个极端之间做出妥协,并受到白盒测试和代码覆盖工具的影响,我将在后续介绍这些工具

讲义原文地址

Reading 3: Testing