2021-06-28


哈工大软件构造阅读心得3-1: 测试


本文参考:MIT Reading3哈工大学长汉化

注:这个系列是本人看过阅读资料之后,对看过的内容进行的总结

验证

验证的目的就是发现程序中的问题,以此提升你对程序正确性的信心。验证包括:

  1. 形式推理,即通过理论推理证明程序的正确性。形式推理目前还缺乏自动化的工具,通常需要漫长的手工计算。即使是这样,一些关键性的小程序也是需要被证明的,例如操作系统的调度程序、虚拟机里的字节码解释器,或者是文件系统

  2. 代码审查。即让别人仔细的阅读、审校、评价你的代码,这也是发现bug的一个常用方法,我们会在下一个reading里面介绍这种方法。

  3. 测试。即选择合适的输入输出用例,通过运行程序检查程序的问题。

阅读小练习

测试基础

阿丽亚娜5型火箭,为欧洲空间局研发的民用卫星一次性运载火箭,名称来源于神话人物阿丽雅杜妮(Ariadne)的法语拼写。1996年6月4日,在风和日丽的法属圭亚那太空发射场,阿丽亚娜5型运载火箭首航,计划运送4颗太阳风观察卫星到预定轨道。但在点火升空之后的40秒后,在4000米高空,这个价值5亿美元的运载系统就发生了爆炸,瞬间灰飞烟灭化为乌有。

爆炸原因由于火箭某段控制程序直接移植自阿丽亚娜4型火箭,其中一个需要接收64位数据的变量为了节省存储空间而使用了16位字节,从而在控制过程中产生了整数溢出,导致导航系统对火箭控制失效,程序进入异常处理模块,引爆自毁。

这个故事告诉了我们什么?

[x] 即使是高度关键性的程序也可能有bug

[ ] 测试所有可能输入是解决这样的问题的最好办法

[x] 与很多物理工程学上的系统不同,软件的行为是离散的

[ ] 静态检查有助于发现这个bug

注:静态类型检查不会检测到此错误,因为代码有意(强转)将64位精度转换为16位精度。

测试优先编程

测试开始的时间应该尽量早,并且要频繁地测试。当你有一大堆未经验证的代码时,不要把测试工作留到最后。把测试工作留到最后只会让调试的时间更久并且调试过程更加痛苦,因为你的代码将会充斥着bug。反之,如果你在编码的过程中就进行测试,情况就会好的多。

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

  1. 为函数写一个规格说明。

  2. 为上一步的规格说明写一些测试用例。

  3. 编写实际代码。一旦你的代码通过了所有你写的测试用例,这个函数就算完成了。

规格说明描述了这个函数的输入输出行为。它确定了函数参数的类型和对它们的所有约束(例如sqrt函数的参数必须是非负的)。它还定义了函数的返回值类型以及返回值和输入之间的关系。在代码中,规格说明包括了函数签名和一些描述函数功能的注释。我们将会在接下来的几节课里讨论更多关于规格说明的问题。

先尝试编写测试用例,可以在你浪费时间实现一个有问题的规格说明之前发现这些问题。

通过分区的方法选择测试用例

在这里插入图片描述

我们可以先将输入空间划分为几个子域(subdomains)
,每一个子域都是一类相似的数据。如上图所示,我们在每个子域中选取一些数据,它们合并起来就是我们需要的输入用例。

例子1: BigInteger.multiply()

现在让我们来看一个例子。
BigInteger
是Java库中的一个类,它能够表示任意大小的整数。同时,它有一个multiply
方法,能够对两个BigInteger类型的值进行相乘操作:

/**

* @param val another BigInteger

* @return a BigInteger whose value is (this * val).

*/
public BigInteger multiply(BigInteger val)

例如,计算ab的值:

BigInteger a = ...;

BigInteger b = ...;

BigInteger ab = a.multiply(b);

这个例子显示即使只有一个参数,这个操作实际上有两个操作符:你调用这个方法所在的对象(上面是a
),以及你传入的参数(上面是b )。我们可以把 multiply
看成一个有两个参数的方法,参数的类型是 BigInteger ,并且输出的类型也是
BigInteger 即:

multiply : BigInteger × BigInteger → BigInteger

所以我们的输入空间是二维的,用二维点阵(a,b)表示。现在我们对其进行分区:

  1. a和b都是正整数

  2. a和b都是负整数

  3. a是正整数,b是负整数

  4. b是正整数,a是负整数

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

即a或b是1\0\-1

最后,作为一个认真的测试员,我们还要想一想BigInteger的乘法可能是怎么运算的:它可能在输入数据绝对值较小时使用
int 或 long
,这样运算起来快一些,只有当数据很大时才会使用更费劲的存储方法(例如列表)。所以我们也应该将对数据的大小进行分区:

  1. a或b较小

  2. a或b的绝对值大于Long.MAX_VALUE ,即Java原始整型的最大值,大约是2^63。

现在我们可以将上面划分的区域整合起来,得到最终划分的点阵:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MwEskeG6-1624888566350)(media/1b4a5b1636ae07cde73e3f4ad19bf837.png)]

  1. 0

  2. 1

  3. -1

  4. 较小正整数

  5. 较小负整数

  6. 大正整数

  7. 大负整数

所以我们一共可以得到 7 × 7 = 49
个分区,它们完全覆盖了a和b组成的所有输入空间。然后从这个“栅栏”里的每个区选取各自的测试用例。

例子2:max()

现在我们看看Java库中的另一个例子:针对整数int的max() 函数,它属于Math 类:

/**

* @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 × int → int

在这里插入图片描述

通过描述分析,我们可以将其分区为:

  1. a < b

  2. a = b

  3. a > b

注意分区之间的“边界”

我们在分区后,测试用例不要忘了加上边界上的值,现在重新做一下上面那个例子:

max : int × int → int.

分区:

  1. a与b的关系

  2. a < b

  3. a = b

  4. a > b

  5. a的值

  6. a = 0

  7. a < 0

  8. a > 0

  9. a = 最小的整数

  10. a = 最大的整数

  11. b的值

  12. b = 0

  13. b < 0

  14. b > 0

  15. b = 最小的整数

  16. b = 最大的整数

覆盖分区的两个极限情况

  1. 完全笛卡尔乘积

即对每一个存在组合都进行测试。例如在第一个例子multiply中,我们一共使用了 7 × 7 =
49 个测试用例,每一个组合都用上了。对于第二个例子,就会是 3 × 5 × 5 =
75个测试用例。要注意的是,实际上有一些组合是不存在的,例如 a < b, a=0, b=0。

  1. 每一个分区都被覆盖

即每一个分区至少被覆盖一次。例如我们在第二个例子max中只使用了5个测试用例,但是这5个用例覆盖到了我们的三维输入空间的所有分区。

练习

分区

思考下面这个规格说明:

/**

* Reverses the end of a string.

* For example: reverseEnd(“Hello, world”, 5) returns “Hellodlrow ,”

* With start == 0, reverses the entire text.

* With start == text.length(), reverses nothing.

* @param text non-null String that will have its end reversed

* @param start the index at which the remainder of the input is reversed,
requires 0 <= start <= text.length()

* @return input text with the substring from start to the end of the string
reversed

*/

public static String reverseEnd(String text, int start)

对于 start 参数进行测试,下面的哪一个分区是合理的 ?

[ ] start = 0, start = 5, start = 100

[ ] start < 0, start = 0, start > 0

[x] start = 0, 0 < start < text.length(), start = text.length()

[ ] start < text.length(), start = text.length(), start > text.length()

注:要特别注意的是,本文谈到的都是对程序正确性进行测试,即输入都是规格说明里面的合法值。至于那些非法的值则是对鲁棒性(robust)或者安全性的测试。

对于 text 参数进行测试,下面的哪一个分区是合理的 ?

[ ] text 包含一些数字; text不包含字母, 但是包含一些数字; text
既不包含字母,也不包含数字

[ ] text.length() = 0; text.length() > 0

[x] text.length() = 0; text.length()-start 是奇数; text.length()-start 是偶数

注,这个选项是第二个的超集,多的地方在于奇数偶数的判断,原因在于如果一个字符串字符的个数是奇数个,那么中间的那个字符就不需要移动位置了,这可能需要特殊的行为来处理,也可能是bug产生的原因

[ ] 测试0到100个字符的所有字符串

用JUnit做自动化单元测试

一个良好的测试程序应该测试软件的每一个模块(方法或者类)。如果这种测试每次是对一个孤立的模块单独进行的,那么这就称为“单元测试”。单元测试的好处在于debug,如果你发现一个单元测试失败了,那么bug很可能就在这个单元内部,而不是软件的其他地方。

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

例如,我们对上面提到的 Math.max() 模块进行测试,JUnit就可以这样写:

在这里插入图片描述

要注意的是assertEquals的参数顺序很重要。它的第一个应该是我们期望的值,通常是一个我们算好的常数,第二个参数就是我们要进行的测试。记住,所有JUnit支持的断言都要写成这个顺序:第一个是期望值,第二个是代码测试结果。

如果一个测试断言失败了,它会立即返回,JUnit也会记录下这次测试的失败。一个测试类可以有很多
@Test
方法,它们可以各自独立的进行测试,即使有一个失败了,其它的测试也会继续进行。

写测试策略

现在假设我们要测试reverseEnd这个模块:

在这里插入图片描述

我们应该在测试时记录下我们的测试策略,例如我们是如何分区的,有哪些特殊值、边界值等等:

在这里插入图片描述

另外,每一个测试方法都要有一个小的注解,告诉读者这个测试方法是代表我们测试策略中的哪一部分,例如:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qAItJvG4-1624888566356)(media/aa3cd6f8aacf6185eef446994808ab5c.png)]

阅读

假设你在为max(int a,int
b)写测试,它是属于Math.java的.并且你将JUnit测试放在MathTest.java文件中。

下面这些文字说明应该分别放在哪里?

关于a参数的分区策略

[]写在Math.java开头的注释里

[x]写在MathTest.java开头的注释里

[]写在max()开头的注释里

[]写在JUnit测试的注释里

属性@Test

[]在Math之前

[]在MathTest之前

[]在max()之前

[x]在JUnit测试之前

注释“代表a<b”

[]写在Math.java开头的注释里

[]写在MathTest.java开头的注释里

[]写在max()开头的注释里

[x]写在JUnit测试的注释里

注释“@返回a和b的最大值”

[]写在Math.java开头的注释里

[]写在MathTest.java开头的注释里

[x]写在max()开头的注释里

[]写在JUnit测试的注释里

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值