1、彻底测试的可能性
    有一种说法是,“测试总是不彻底的”,对于软件整体来说,要做到彻底测试确实很难,但对于局部代码,则是完全可能的。先说 “理想的彻底测试”,对于一段程序,比如一个函数,如果它的所有可能输入都测试过,并且都没有错误,那么可以肯定,这个函数是没有错误的,这是 “理想的彻底测试”。“理想的彻底测试”是做不到的,有没有 “现实的彻底测试”?有!“现实的彻底测试”是所有的等价类都经过测试。
    先说说等价类。“类”是指把输入数据分组,“等价”是指测试效果上的等价,“等价类”是在一组输入数据中,任取一个进行测试,如果测试通过,那么,可以认为同组的其他输入数据也可以测试通过,也是说,测试了一个,等于测试了一组。如果把所有的等价类都找出来进行测试,相当于所有输入都经过测试,这是“现实的彻底测试”。
    对于一段程序来说,“等价类”通常是有限的,因此,从理论上来说,对局部代码进行“现实的彻底测试”是完全可能的。
    2、等价类划分的基本方法
    通常从三个方面考虑程序的输入:正常输入,边界输入,非法输入。
    正常输入:例如字符串的Trim函数,功能是将字符串前后的空格去除,那么正常的输入可以有四类:前面有空格;后面有空格;前后均有空格;前后均无空格。
    边界输入:上例中空字符串可以看作是边界输入,如果输入是一个指针的话,空指针也算是边界输入。再如一个表示年龄的参数,它的有效范围是0-100,那么边界输入有两个:0和100。
    非法输入:非法输入是正常取值范围以外的数据,或使代码不能完成正常功能的输入,如上例中表示年龄的参数,小于0或大于100都是非法输入,再如一个进行文件操作的函数,非法输入可能有:文件不存在;目录不存在;文件正在被其他程序打开;权限错误。
    一般情况下,只要考虑:有哪些正常输入?有哪些边界输入?有哪些非法输入?可以找出大多数等价类,例如,一个函数,功能是把小写金额转换成大写,输入是小写数字,输出是大写金额,那么:
    正常输入有:只有整数,只有小数,既有整数又有小数
    边界输入有:整数部分很大,0.0,小数位数超过两位
    非法输入有:空串,非数字,负数
    “等价类”这个词是从测试的角度来说的,从开发的角度来看,“等价类”与“功能点”具有对应关系,例如,字符串的Trim函数,功能点有:
    如果只有左边有空格,返回删除左边空格后的结果(等价类:左边有空格);
    如果只有右边有空格,返回删除右边空格后的结果(等价类:右边有空格);
    如果两边都有空格,返回删除两边空格后的结果(等价类:两边有空格);
    如果两边都没有空格,返回原串(等价类:两边无空格);
    如果是空串,直接返回(等价类:空串);
    如果是空指针,直接返回(等价类:空指针);
    ……
    一个“等价类”对应程序的一个“功能点”,如果程序的所有功能点都正确实现了,那么这个程序的功能肯定没问题。程序员在写代码时肯定要想清楚程序的主要功能点,否则代码无从写起,因此,如果程序员边编码边测试的话,大部分等价类都是现成的。如果由测试部门做单元测试,测试部门只能依据设计文档来测试,设计文档也会规定程序的功能,要不然无从测起,同样可以说大部分等价类是现成的。
    3、彻底测试的基本思路
    前面说过:如果把所有的等价类都找出来进行测试,相当于所有输入都经过测试,这是“现实的彻底测试”。
    如何把所有的等价类都找出来?根据程序的功能,大部分等价类都是容易想到的,甚至是现成的,但是,如何衡量是否完整?例如,前面所举的两个例子:删除字符串两边空格的函数和将金额小写转大写的程序,我们可以肯定,等价类多也十几个,相当有限,但是,已经列出的等价类是完整的吗?有没有遗漏的?
    要找出所有等价类,关键是要有衡量完整性的指标。等价类实际上是程序的功能点,程序的功能是人为规定的,很难衡量完整性,但白盒测试却很容易衡量完整性,因此,我们可以考虑使用白盒测试的覆盖率来衡量测试完整性。
    看一下这个简单的例子:
    void Func(int* p)
    {
    if(p)
    {
    *p = 0;
    }
    else
    {
    return;
    }
    }
    参数p是一个指针,测试时当然要将空指针作为一个等价类,如果漏了这个等价类,会怎么样呢?分支覆盖会不完整:else分支未覆盖。从这个例子可以看出,未覆盖的逻辑单位通常对应未测试的等价类,因此,白盒覆盖可以衡量等价类是否完整。

$nes_page$

    还是上面的例子,假如程序员完全忘了有空指针这回事,把代码写成这样:
    void Func(int* p)
    {
    *p = 0;
    }
    由于判断p是否为空指针的代码不存在,白盒覆盖当然不会提示说“某某代码或某某分支未覆盖”,因此,白盒覆盖不能发现“程序员未处理某些特殊输入”这种情形,也是说,即便达到了无与伦比的白盒覆盖率,仍然不能保证找出所有等价类。
    程序员会忘记处理哪些输入呢?常见的输入一般是不会记的,否则程序的起码功能都未实现,容易忘记的是一些“偏僻”的输入,例如,空指针、空字符串、很大的数、很小的数、合法取值边界附近的值等等,从输入的角度来看,这些特殊值通常跟数据类型有关,从程序的行为来看,这些特殊输入常常会导致崩溃、产生异常,或超时,即具有行为特征,正好是自动动态测试可以发现的,因此,我们可以利用自动动态测试来捕捉“程序员未处理某些特殊输入”形成的错误。
    4、彻底测试方法
    基于上一节的思路,经过长时间摸索和改进,我们提出了实现彻底测试的“三步法”:
    1)根据代码的功能,人工设计测试用例进行基本功能测试;
    2)统计白盒覆盖率,为未覆盖的白盒单位设计测试用例,实现完整的白盒覆盖,比较理想的覆盖率是实现语句、条件、分支、路径覆盖;
    3)自动生成大量的测试用例,捕捉“程序员未处理某些特殊输入”形成的错误。
    第1步的测试用例通常是现成的,因为详细设计文档会规定程序的基本功能,没有文档的,程序员在编程时也要想清楚程序的功能,这些基本功能是基本测试用例;
    第2步是在第1步的基础上,检查未覆盖的白盒单位,由于未覆盖的逻辑单位通常对应未测试的等价类,因此第2步可以找出第1步所遗漏的测试用例;
    第3步用自动动态测试弥补第2步的固有缺陷。
    “三步法”尽量避免重复工作,白盒方法和黑盒方法相结合,人工方法和自动方法相补充,如果第2步的覆盖率比较理想,那么基本上可以保证找出所有等价类。在开发过程允许的限度内,“三步法”已接近极限,当得起“彻底测试”四个字。
    5、是否需要其他测试?
    “三步法”使用了人工动态测试和自动动态测试,未使用人工静态分析和自动静态分析,要不要再使用这两种方法呢?这要看合不合算了,毕竟项目时间和预算是有限的。
    “三步法”的测试完整性是空前的,读者可以比较一下,自己参与过的项目,所要求达到的覆盖率是怎么样的?做得比较好的,一般只是要求达到的代码覆盖,执行中还未必能做到,而“三步法”所要达到完整性是这样的:1)语句、条件、分支、路径覆盖;2)用自动动态测试捕捉未考虑某些特殊输入形成的错误。经过这种彻底测试后,遗留的错误可以说已经极少了。
    自动静态分析只能发现语法特征错误,现代编译器对多数这类错误会给出警告,另外,现有的自动静态分析技术会产生大量的误报,在一大堆报告中去一条一条人工辨别,寻找遗漏的错误,无异于“大海描针”。人工静态分析也差不多。先做这两项又怎么样?显然也是没必要的。
    为了统一代码风格,可以偶尔抽查代码,也是说,可以用人工静态方法检查代码风格和编码规范。