【程序设计实践】第6章 测试

6章 测试

测试和排错常常被说成是一个阶段,实际上它们根本不是同一件事。简单地说,排错是在你已经知道程序有问题时要做的事情。而测试则是在你在认为程序能工作的情况下,为设法打败它而进行的一整套确定的系统化的试验。

测试能够说明程序中有错误,但却不能说明其中没有错误。

在编码过程中测试

1)  测试代码的边界情况。一项重要技术是边界条件测试:在写好一个小的代码片段之后,就应该检查条件所导致的分支是否正确,循环实际执行的次数是否正确等。这种工作称为边界条件测试,因为你的检查是在程序和数据的自然边界上。

2) 测试前条件和后条件。防止问题发生的另一个方法,是验证在某段代码执行前所期望的或必须满足的性质(前条件)、执行后的性质(后条件)是否成立。

3) 使用断言CC++里提供了一种断言机制,它鼓励给程序加上前/后条件测试。断言失败将会终止程序,所以这种机制通常是保留给某些特殊情况使用的,写在这里的错误是真正不应该出现的,而且是无法恢复的。我们可能在前面程序段里的循环前面加一个断言:assert(n > 0);

4) 防御性的程序设计。有一种很有用的技术,那就是在程序里增加一些代码,专门处理所有“不可能”出现的情况,也就是处理那些从逻辑上讲不可能发生,但是或许(由于其他地方的某些失误)可能出现的情况。设法使程序在遇到不正确使用或者非法数据时能够保护自己:空指针、下标越界、除零。

5) 检查错误的返回值。一个常被忽略的防御措施是检查库函数或系统调用的返回值。对所有输入函数(例如freadfscanf)的返回值一定要做检查,看它们是否出错。对文件打开操作(fopen)也应该这样。如果一个读入操作或者文件打开操作失败,计算将无法正确地进行下去。

系统化测试

1) 以递增方式做测试。测试应该与程序的构造同步进行。与逐步推进的方式相比,以“大爆炸”方式先写出整个程序,然后再一股脑儿做测试,这样做困难得多,通常也要花费更长时间。写出程序的一部分并测试它,加上一些代码后再进行测试,如此下去。如果你有了两个程序包,它们已经都写好并经过了测试,把它们连接起来后就应该立即测试,看它们能否在一起工作。

2) 首先测试简单的部分。递增方式同样适用于对程序性质的测试。测试应该首先集中在程序中最简单的最经常执行的部分,只有在这些部分能正确工作之后,才应该继续下去。这样,在每个步骤中你使更多的东西经过了测试,对程序基本机制能够正确工作也建立了信心。通过容易进行的测试,发现的是容易处理的错误。在每个测试中做最少的事情去发掘出下一个潜在问题。虽然错误可能是一个比一个更难触发,但是可能并不更难纠正。

3) 弄清所期望的输出。对于所有测试,都必须知道正确的答案是什么,如果你不知道,那么你做的就是白白浪费自己的时间。

4) 检验应保持不变的特征。许多程序将保持它们输入的一些特征。有些工具,如wc(计算行、词和字符数)sum(计算某种检验和)可用于验证输出是否与输入具有同样大小、词的数目是否相同、是否以某种顺序包含了同样的字节以及其他类似的东西。另外还有一些程序可以比较文件的相等(cmp)或报告差异(diff)等。这些程序,或其他类似的东西在许多环境里都可以直接使用,也是非常值得用的。

5) 比较相互独立的实现。一个库或者程序的几个相互独立的实现应该产生同样的回答。例如,由两个编译程序产生的程序,在相同机器上的行为应该一样,至少在大部分情况下应该是这样。

6) 度量测试的覆盖面。测试的一个目标是保证程序里的每个语句在一系列测试过程中都执行过,如果不能保证程序的每一行都在测试中至少经过了一次执行,那么这个测试就不能说是完全的。完全覆盖常常很难做到,即使是不考虑那些“不可能发生”的语句。设法通过正常输入使程序运行到某个特定语句有时也是很困难的。

测试自动化

自动回归测试。自动化的最基本形式是回归测试,也就是说执行一系列测试,对某些东西的新版本与以前的版本做一个比较。在更正了一个错误之后,人们往往有一种自然的倾向,那就是只检查所做修改是否能行,但却经常忽略问题的另一面,所做的这个修改也可能破坏了其他东西。回归测试的作用就在这里,它要设法保证,除了有意做过的修改之外,程序的行为没有任何其他变化。

建立自包容测试。自包容测试带着它们需要的所有输入和输出,可以作为回归测试的一种补充。

测试台

要孤立地测试一个部件,通常必须构造出某种框架或者说是测试台,它应能提供足够的支持,并提供系统其他部分的一个界面,被测试部分将在该系统里运行。

应力测试

采用由大量机器生成的输入是另一种有效的测试技术,机器生成的输入对程序的压力很大,对于发现某些问题,是很有效果的。机器不会回避任何东西,严格按照它的程序生成。

随机输入测试,考察的是程序的内部检查和防御机制。

测试秘诀

程序都要检查数组的界限

让散列函数返回某个常数值,让所有元素跑到一个桶里。指明最坏情况的性能。

写一个自己的存储分配函数,有意让它早早失败,利用它测试存储器耗尽的情况。

提交代码的时候,应关掉所有的测试限制。

把数组和变量初始化为某个可辨认的值,不要总是默认为0.

变动你的测试实例,尤其在手工做小测试时。

如果发现有错误存在,就不要继续实现新的特征或者再去测试已有的东西。

测试输出应包括所有的参数设置,使人容易准确重做同样的测试

提供一种方法来控制程序运行输出的量和种类

在不同的机器、编译系统、操作系统上测试。

谁来测试

由程序实现者或其他可以接触源代码的人做的测试有时也被称为白箱测试。

黑箱测试的测试者对代码的内部结构毫不知情,也无法触及。

实际用户接踵而至。新用户往往能发现新错误,因为他们会以我们无法预知的方式来探测这个程序。

测试交互式程序是特别困难的,特别是如果它们还涉及到鼠标输入等。有些测试可以用脚本来做(脚本的特性依赖于语言、环境和其他类似东西)

最后,还应该想一想如何测试所用的测试代码本身。

测试马尔可夫程序

第一个测试集由几个很小的文件组成,用于测试边界条件,目标是保证程序对只包含几个词的输入能正确产生输出。

第二项测试检验某些必须保持的特征。对于两词前缀的情况,一次运行中输出的每个词、每个词对、以及每个三词序列都必然也出现在输入里。

第三步测试是统计性的。

最后,我们给马尔可夫程序普通的英文文本,看着它产生出很漂亮的无意义的费话。

所有这些测试都是机械化的。用一个脚本产生必需的输入数据,运行测试并且对它们计时,打印出反常的输出。这个脚本本身是可配置的,所以同一个测试能够应用到马尔可夫程序的各种版本上,每次我们对这些程序中的一个做了更改后,就重新运行所有测试,以保证所有东西都没出问题。

小结

你把开始的代码写得越好,它出现的错误也就越少,你也就越能相信所做过的测试是彻底的。在写代码的同时测试边界条件,这是去除大量可笑的小错误的最有效方法。系统化测试以一种有序方式设法探测潜在的麻烦位置。同样,毛病最可能出现在边界,这可以通过手工的或者程序的方式检查。自动进行测试是最理想的,用得越多越好,因为机器不会犯错误、不会疲劳、不会用臆想某些实际无法工作的东西能行来欺骗自己。回归测试检查一个程序是否能产生与它们过去相同的输出。在做了小改变之后就测试是一种好技术,能帮助我们将出现问题的范围局部化,因为新问题一般就出现在新代码里面。

对于测试,惟一的、最重要的规则就是必须做。


你可能感兴趣的:(程序设计实践)