测就测呗,有啥难的!

3、如何进行单元测试?(3)

上一篇 / 下一篇  2009-03-27 11:33:58 / 个人分类:单元测试

3.4测试用例

测试用例是整个单元测试工作的核心,这一节也是我们整个讲座最重要的一节。

3.4.1用例的构成

测试用例主要由两部分构成:一个是输入数据,一个是预期输出。通常我们是先选择输入,然后针对确定的输入,根据程序的设计功能推算预期的正确输出,这样就构成了一个测试用例。

输入就是被测试程序需要读取的数据,具体来说包括:1、参数。2、需要读取的成员变量,这是对C++或者别的面向对象语言来说的。3、需要读取的全局变量。4、内部输入。

如果输入是复杂类型则只需关注确实需要读取的部分,其他是可以不管的。

内部输入是指调用子函数获得的输入。函数MyFunc1中的局部变量area是通过调用一个子函数来获得它的初始值,所以它是一个内部输入。局部变量ptr也是调用一个子函数来获得它的内存,所以也是一个内部输入。

int MyFunc1(int a, int radius)
{
    double area = GetArea(radius);

     ......
   int* ptr = malloc(1024);
   if(!ptr)
       ....
}

内部输入分为两类,一类是可以间接设置的,也就是可以通过外部传递一些数据来间接设置内部输入的。还有一类是不可间接设置的,完全就是由系统控制的。在这一个例子(上面例子)当中,第一个内部输入是可以间接设置的,第二个是不可以间接设置的。

一般来说测试比较高层的代码,都会存在大量这种需要用来传递给子函数的输入,我们把这种仅仅传递给子函数的输入称为间接输入。实际上我们为了控制测试流程,需要一个合适的内部输入。为了获得这个合适的内部输入,我们通常需要去推算一个合适的间接输入,这是很麻烦的事情,也是影响测试效率的瓶颈之一。高层的代码很难测试,往往就是这个原因。

预期输出是用例必不可少的组成部分,缺少预期输出,测试基本上没有意义。预期输出包括什么呢?从概念上来说预期输出是指:正确的返回值以及函数改写的数据的正确结果。函数改写的数据,除了返回值外,还可能包括:输出参数、函数所改写的成员变量、函数所改写的全局变量。有时还可能需判断中间变量,这也算是一种内部的输出,我们前面讲到内部的输入,有时候还需要判断内部的输出。有一个原则就是复杂数据类型,我们只需判断实际改写的部分,其他无关的就不用管。

3.4.2设计测试用例的方法

设计测试用例的一般方法首先就是建立输入,函数的可能输入往往是无限的,只能选择有代表性的部分输入进行测试。输入一般有三大类:正常输入,边界输入,非法输入。

大类还可以进一步分为小类,分小类的依据是:在该小类中任取一个输入,如果测试通过,那么可以确认其他输入也会测试通过,这个就是我们平常说的等价类法。等价是指测试效果上的等价。

我们用下面这个例子来说明等价类。比如我们要测试一个函数:char* strtrm(char* pstr);

它的功能是去除字符串两边的空格,输入有下面几种情况

v      ABCD      (右边有空格)

v        ABCD   (左边有空格)

v     ABCD     (两边有空格)

v     ABCD       (两边无空格)

v     “”           (空串,边界输入)

v       NULL         (空指针,非法输入)

前面这四种都可以说是正常的输入,

上面所讲的等价类可能有些抽象,我们可以从另外一个角度来阐述这个等价类。即使我们现在不考虑测试,如果要编写比较健壮的程序,也要考虑这个程序有哪些正常的输入,如何对这些正常的输入进行处理,程序会不会有无需特别处理的边界输入,我们又如何去处理这些边界的输入,这些程序会不会有非法的输入进来,我们又需要如何去防御。这些就是开发时候需要考虑的“功能点”,在测试中,就是“等价类”。

一般来说我们要设计用例,首先要了解程序的设计功能,然后把程序功能细化、明确化,列成什么输入,应产生什么输出的形式,这就是测试用例。它跟开发时候考虑的功能点是很相似的,甚至可以说重叠的。

通常,输入都不止是一个数据,也就是说不止一个参数,它可能会涉及到多个数据,比如说多个参数,可能还涉及到成员变量,前期变量,内部输入这些数据。那我们如何去设计测试用例?通常是每个数据考虑它的正常输入、边界输入、非法输入,然后再考虑这些输入当中哪些值是需要组合起来进行测试的。把这两个组合起来,基本就可以建立完整的输入了。

接着,我们要依据程序的设计功能设定预期输出,绝不能通过阅读代码来推算预期输出。也就是说在某种输入确定的情况下,我们要根据程序的功能来推算什么输出是正确的,这个就是预期的输出。

v     示例:
int Add(int a, int b){return a-b;};

上面这个例子中把加法函数中“+”写成“-”,如果我们知道这个程序功能,输进去两个“1”,预期输出应该是一个“2”。如果我们不知道这个程序的功能,就纯粹根据这个代码功能来设定预期输出,输进去两个“1”,我们就给它设为一个“0”。如果是这样子的测试,用例再多也是没有什么效果的。

3.4.3用例的完整性

用例的完整性,也就是说如何去实现完整的测试。至于什么是好的用例呢?有一个说法是发现了错误的用例就是好用例,或者说一个好用例就是发现了讫今没有发现的错误的用例。我们认为这是一种误导。假设代码完全没有错误,那所有的用例都是坏用例吗?或者说现在代码有错,这个用例就是好用例。更改了以后,在回归测试的时候没有错了,难道这个用例就是坏用例了吗?这显然是很难指导工作的,我们认为好用例是一个组合,它应该能覆盖所有等价类。举个例子,假设程序是一个池塘,其中的错误就是池塘里的鱼,用例是网。用网来捞鱼,只要用例是一个合格的网,并且能覆盖整个池塘。那池塘里有鱼的话,一网就能捞上来。这个网就是好网,符合要求的网。如果网是完整的,合格的,并且能覆盖整个池塘,捞不上鱼,就证明池塘里是没有鱼的。网的好坏与是否有鱼完全无关的。

具体来说用例的理想状况应该是:等价类划分是准确的,并且等价类是完整的。等价类划分是准确的就是说每个等价类都保证:只要其中的一个输入测试通过,其他输入也一定通过。等价类是完整的就是说所有的等价类都已找出来并且已通过测试。只有做到这两点,我们才可以肯定测试是充分的,能做到了完整的输入覆盖,也可以说任何的输入都是没有问题的。

现在面临的问题是如何知道等价类是否准确和完整。我们可以通过白盒覆盖、自动边界测试、分类集中法来检验。

因为程序的功能是人为的规定,它到底有哪些输入,通常很难用数字来衡量,所以我们通常用一种间接的方法来衡量输入的完整,这就是白盒覆盖。白盒覆盖可以衡量用例的完整性,可以找出遗漏等价类。但是白盒覆盖也不是万能的,因为白盒覆盖是基于现有代码的,假如我们编码的时候没有考虑到某些输入,这种情况下白盒覆盖是发现不了的。也就是说假如我们的代码,一个函数直接就返回一个值了,这种情况下任何的白盒覆盖都是百分百的。像这种错误,白盒覆盖是毫无能力的。

我们可以用一些别的方法来弥补白盒覆盖的不足。比如可以用自动边界测试来捕捉一部分“未考虑某些输入”形成的错误。因为“未考虑某些输入”经常是边界的输入,这种输入很可能会产生异常,崩溃这种极端的错误,用自动边界测试很可能就能捕抓到。

还有一种方法就是分类集中法,人工检查数据完整性。也就是说先把数据集中起来,然后进行检查。它是一个非常常用的方法。比如说一个幼儿园有几个班,每个班的孩子到齐了没有?如果这些孩子都混在一块,那么很难搞清楚那个班的孩子到齐了没有。如果把他们分开,把每个班的集中到一起,就很容易搞清楚了。这个就是分类集中法。

如果把这三种方法结合起来就可以实现非常完整的覆盖。

常用的白盒覆盖率主要有:语句覆盖、条件覆盖、条件值覆盖、条件值组合覆盖、分支覆盖、路径覆盖。覆盖率就是已经覆盖的逻辑单位占所有逻辑单位的比例。每种覆盖都有它的不足之处,通常我们需要一个组合来达到比较好的覆盖效果。如果是从找出遗漏用例的角度来看,经过比较多的实践,我们认为用语句覆盖,条件覆盖,分支覆盖,路径覆盖的组合效果是比较好的,也就是说比较理想的覆盖率应该是100%语句覆盖,条件,分支,和路径覆盖。

我们前面提到可以用白盒覆盖来衡量测试的完整性,也提到过白盒覆盖的局限性。具体来说白盒覆盖是基于已存在的代码,如果程序员在写代码的时候,有一些代码他没有考虑到,那相关的代码他肯定也是没有写出来的。在这种情况下,白盒覆盖是发现不了的。这就跟程序员测试自己的代码的局限性是一致的,程序员测试自己的代码的思维跟开发思维是重叠的,如果某些输入他开发时没有考虑到,那么测试的时候他也不会考虑到。

另外白盒覆盖只能衡量输入完整性,不能衡量有没设定输出。在实际开发当中,不要片面去追求覆盖率,我们应该考虑通过比较完整的输入覆盖来衡量测试的完整性,而不仅仅是纯粹一个白盒覆盖率。白盒覆盖只是输入覆盖的一个衡量的方面而已。

白盒覆盖可以用来找出遗漏的测试用例,我们用下面这个简单的例子来说明它怎样找出遗漏的测试用例。

void Func(int* p)

{

   if(p)

   {

       *p = 0;

   }

   else

   {

       return;

   }

}

假如我们在设计测试用例的时候没有考虑到这个参数有可能是一个空指针,那么空指针没有作为一个用例来测试会怎样呢?白盒覆盖会告诉我们这个else分支没有覆盖到,从而可以知道这个空指针我们没有测试到,这就是可以找出遗漏的用例。

前面说过白盒覆盖有它的局限性,它是不能发现“忘记处理某些输入”形成的错误。下面这个例子就可以说明这个问题:

void Func(int* p)

{

   *p = 0;

}

假设程序员在编码的时候根本就没考虑到空指针这么一个输入,于是直接就赋值了。这样会形成一个严重的错误,但是白盒覆盖对此是完全无能为力的。有一个好处就是容易忘记的往往是边界输入,可能会导致崩溃,异常,超时这些有特征的错误。而这些恰好就是自动边界测试的捕抓对象。如果这些忘掉的输入不是边界的输入,也不会产生异常,超时等这些有特征的错误,那么自动的边界测试就不能发现它。为了解决这个问题,我们可以使用分类集中法。

分类集中法就是把数据集中起来进行人工的检查,下图是一个分类集中法的截屏的界面,它首先是把每一个数据的取值给列出来,然后再看有哪些组合是必须要测试的,把它选上。这样我们就可以比较容易检查输入的完整性,这就是人工的检查。如果把这些数据分散在很多代码里边的,就很难搞清楚它齐不齐。这就是分类集中法。如果我们把分类集中法、白盒覆盖以及自动的边界测试三者结合起来,那么输入覆盖率就是相当高了。


TAG:

 

评分:0

我来说两句

日历

« 2024-05-08  
   1234
567891011
12131415161718
19202122232425
262728293031 

数据统计

  • 访问量: 4872
  • 日志数: 10
  • 建立时间: 2008-10-22
  • 更新时间: 2009-03-30

RSS订阅

Open Toolbar