测就测呗,有啥难的!

发布新日志

  • 4、单元测试的难题以及解决的思路。

    2009-03-30 09:40:41

    测试少量独立的代码,是很简单的事,但在实际工作中完全是另一回事。学单元测试,通常不会觉得它很难,一两天的时间就可以了解基本的方法,那应该说是一种简单的技术。但实际上,如果要在实际的项目当中完成单元测试,常常觉得到处都是障碍,甚至会觉得寸步难行,因为其中一些难题没解决。下面分别讲单元测试的难题

    一、代码的可测性。代码通常各部分都是互相关联的,但单元测试却是需要把代码单元跟别的代码分开进行测试。这样就会产生一个隔离的问题,也就是代码的可测性问题。项目规模越大,可测性一般越差,具体来说就是我们想测试某一个代码单元,但是没办法对它进行独立的编译链接,然后独立的执行。解决这个代码的可测性问题,理论上可以用强化设计,减少耦合这种方式来做,而且这种方式可以提高代码的整体质量。但实际上比较难做到,因为代码它实际上反映的是客观世界,客观规律。客观世界各种事物本身就是互相关联,互相交缠的,那么代码也是很难各个模块独立的。比较现实的方法是由工具自动打桩来解耦合,也就是说把相关联的代码用桩代码来代替,这样就可以解决可测性的问题了。

    二、关联的模块未开发。这个很正常,项目通常都是并行开发的,每一个程序员他都可能要调用到别的同事开发的代码,但是这些代码还没有开发出来。这个时候也就难以调试,测试。解决的思路是用工具来打桩补齐未定义的符号。

    三、失真。失真是由打桩衍生的难题。为什么打桩会造成失真?因为桩代码通常只是一个最简单的实现,它什么也不做,跟实际的代码是相差很遥远的。

    比如说在下面这个例子:

    struct DATA;
    extern int subfunc(int* pvar, DATA* pdata) //
    :{return 10;};
    int CMyClass::func(int arg, struct DATA* pdata)
    {
        int a, b;
        a = subfunc(&b, pdata);//a
    总为10,b始终未初始化

        if(a <= 10)
        {
            //
    其他代码
            return b;
        }
        else if(b < 0)
        {
            //
    其他代码

            return b;
        }

        //
    其他代码


        return b;
    }

    subfunc这个子函数,我们用一个桩代码来代替的话,工具自动生成的桩代码很可能就是:{return 10;};这么一个样子,返回一个10然后就什么都不做。被测试函数在调用这个桩代码之后,它就是一种固定的状态,a总为10,b始终未初始化。这样子无论输入是什么,后边的这种路径就是一样的。这样会造成测试没有办法进行下去,这就是失真。解决失真的一种办法就是自己去修改桩代码,但在实际工作中自己去修改桩代码也是很难的。因为我们的桩代码它要实现什么样的功能才能代替正式的代码,这太复杂了,而且不同的用例桩代码应该是做不同的动作的。比如说不同的用例当中, ab的值应该是不一样的,你很难在桩代码当中去实现这种控制。失真是必须要解决的一个难题,否则的话测试是很难实现预期的效果的。

    四、不可控。通常都是子函数的行为不可控,然后造成测试难于进行。比如说子函数是访问不存在的硬件,它就没办法完成我们想要的动作。或者说子函数要访问外部的系统,如数据库,网络,这些也可能不存在或者是难于进行真实的这种设置。还有可能会需要产生一些难于出现的状态,比如子函数会产生一个实际数,我们需要知道这个子函数产生的实际数是什么,要设定一个数的话可能就很难了。还有子函数可能是耗时很长的,或者是死循环。这种情况下都是很难进行测试,这些都属于不可控。解决失真和不可控的思路,就是底层模拟,也就是在用例当中来模拟和控制这种子函数的行为。这样就可以避开一些没法测试的情形,然后模拟真实的状态来达到我们的测试预期。

    五、编写驱动。我们为了执行被测试代码通常需要一些驱动。编写这些驱动不难,但是比较费时间,而且没有创造性,比较容易厌烦。那为什么说它是难题,因为它会造成测试的成本高或者是让人家觉得比较麻烦,容易产生比较抵触的心理,造成测试进行不下去或者效果不好。它的解决方法也是比较简单,就是用工具来自动生成驱动。生成驱动不是很高的技术,哪怕你自己去写一个工具也是可以做到这一点的。

    六、复杂初始化。有时候一些输入是相当复杂的,要完成这个初始化挺麻烦。这个时候通常可以找一下有没有一些代码是调用这个被测试函数的。如果有的话,我们是应该可以找到用于初始化的代码。假如没有的话,就需要另外去编写一些完成这些复杂初始化的代码。函数无论多复杂,它都是需要被别的函数调用的。那么这种复杂初始化的代码通常是已经存在的了。用现有的代码来完成这种初始化,只是其中的一种方法。这种方法是用于我们复杂的数据需要在被测试的代码当中真正的直接被读写,它就应该是一个需要实际进行初始化的数据,我们可以调用现有代码来完成初始化。

    有些输入是相当复杂的,一个实际例子:

    int AddPerson(
         
    PERSON* pData, //非常复杂的结构                                                           

            CPersonMap* map)
    {
    }

    七、间接输入。前面讲过,有一些初始化是相当麻烦的,这些初始化,其中有一部分,甚至大部分就是间接输入。也就是说我们的这些输入并不是直接的被测试代码来读写的,而是要传递给底层函数。通常为了获得比较完整的测试覆盖,我们要让内部数据在调用底层函数之后,得到一个合适的值。为了得到这个合适的值,我们需要间接的设置外部输入的值。这需要一个推算的过程,这个工作量是相当高的。我们的解决思路是对这种间接输入不再给它初始化,而是把它设为内部的输入,直接的从内部设置我们需要的数据,这就比较简单。

    下面的例子是一个间接输入的例子,就是说把间接输入转换为直接的内部输入。第二个参数是一个映射表。被测试函数并不直接去读取这个映射表的数据,而是调用了一个子函数去查询这个映射表里面的一些状态。为了让这个查询返回真,我们直接让这个子函数返回我们想要的值,这样就非常简单。如果用一般的初始化方法对这个函数进行初始化,可能就比较麻烦。这就是把间接输入直接转换为内部输入的一个例子。

     

    v      int AddPerson(PERSON* pData,    
                 
    CPersonMap* map)

    {   

          if(map->Search(&pData->name))                  return 0;    
       
       
    map->Add(pData);
       
    return 1;
    }

     

    八、白盒覆盖的完整性。一般来说要自动统计白盒覆盖并不是很难的,但是白盒覆盖有一个特点,越到后边就越难,仅仅只统计出这个覆盖率往往意义不大。也就是说如果统计出来之后,没法去提高这个覆盖率,那这个统计就没多大意义。我们的解决思路是基于现有用例,利用白盒统计找出遗漏用例,这个就是工具要实现的一个功能。

    九、输入覆盖完整性。由于白盒覆盖是基于已编写的代码,对于设计和编码时未考虑和未处理的输入是无能为力的,这是它的局限性。如果我们要实现比较完整的输入覆盖,即使白盒覆盖做得非常好,也是没有保障的。我们提出的思路就是将数据分类集中,这样可以便于人工检查输入完整性,实现比较高的输入覆盖。

    十、避免干扰思维。程序员进行单元测试,是可行的一种方式,也是最普遍的一种方式。但是不能让单元测试去干扰程序员的思维。因为程序员在编写代码的时候是需要非常专注的。如果在编写测试代码,解决测试中碰到的各种问题的时候要花很多时间,就会造成程序员开发思维的中断和干扰,这个是损失比较大的一件事情。我们提出来的解决思路是要由工具来完成其他工作,只有用例的输入输出由人工定义,因此,比较短的时间就能完成单元测试的工作,从而避免干扰思维。设定用例的输入和输出是一种设计工作,对编程对开发是非常有好处的。

    这里我们讲到单元测试的十个难题,这些难题如果要靠手工的方式来解决是非常困难的。我们要使用工具才能解决这些难题。这些难题也是有一个层次的,有一些是必须要解决,否则测试就做不下去,有一些是涉及到效率效果的问题。如果我们要在大中项的项目中实施单元测试,工具应该能做到解耦合、补齐未定义符号、模拟底层代码的行为以及能提供便利可自编代码完成复杂初始化。如果要想高效率测试,工具应该还可以做到:自动生成驱动、将复杂的间接输入转换为简单的内部输入、高度自动化避免干扰思维。如果还要保证测试效果,工具还应该做到:将数据分类集中以保证输入覆盖率、协助找出遗漏用例以保证白盒覆盖率。

    在这个讲座当中,我们完全没有涉及到具体的工具,只是讲技术和解决思路。有了这些具体的思路,我们就可以去寻找合适的工具。我们还有一个讲座是涉及到具体的工具的,这个讲座会使用工具演示的方式来具体介绍如何解决这些难题。

  • 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;

    }

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

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

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

    2009-03-27 11:30:22

    3.3测试代码

    下面介绍测试代码的编写。虽然现在比较自动化的工具不需要手工去编写比较多的代码,但是了解测试代码的基本构成是有一些好处的。这里用的例子是C++代码,因为单元测试面对的是源代码,所以不同的语言稍有一些差别,但是基本的原理还是差不多的。

    示例产品代码:

    v      //MyClass.cpp
    class CMyClass {

    v      public:

    v          int Add(int i, int j)

    v          {
            return i+j;
        };

    v          void Grow(int years)
        {
            mAge += years;
            //
    其他代码

        };     

    v          

    v          CMyClass();

    v          virtual ~CMyClass();

     

    v      private:

    v          int mAge;      //年龄

    v          CString mPhase; //年龄阶段

    v      };

    这个代码有两个函数,一个是简单的加法函数,另一个是涉及到成员变量的函数。我们可以这样来组织测试代码:对于C语言,可以每一个产品文件对应建立一个测试文件。对于C++,可以每一个类对应建立一个测试类。每一个产品函数需要测试的话建立一个对应的测试函数。这样比较简单,也容易维护。

     

    测试类:

    v      class CMyClassTester

    v      {

    v         CMyClass* pObj; //被测试类的对象指针

     

    v         void CaseBegin();    //用例初始化

    v         void CaseEnd();      //用例结束

     

    v         void ClassTest();    //执行本类中的所有测试函数

     

    v         //各个测试函数加到此后

    v         void Test_Add_int_int();

    v         void Test_Grow_int();

    v      };

     

    测试函数:

    v      void CMyClassTester::Add_int_int()

    v      {

    v          //第一个测试用例

    v          {CaseBegin();               //1

    v          int i = 0;                  //2

    v          int j = 0;                  //3

    v          int ret = pObj->Add(i, j);  //4

    v          TestAssert(ret == 0);       //5  

    v       CaseEnd(); }                //6

    v      }

    这个测试函数只有一个测试用例。这个测试用例的结构是非常清晰简单的。首先是一个初始化的语句,最后是一个清理的语句,中间是一个被调用函数的语句。这三个语句在不同的用例中都是很相似的。int i = 0;  int j = 0;  为输入区域,TestAssert(ret == 0);为预期输出区域。一个用例最主要的是它的输入和输出。我们在这个用例的前面和后面加入一个大括号,目的是让这个用例自成一个域,不同的用例可以使用相同的变量名,方便我们建立更多的用例。

    使用拷贝和修改可以建立更多的用例,以下测试函数的第二个用例跟第一个用例相比只是输入和输出不同,其他都是完全一样的。拷贝过来修改一下输入输出就是新的用例了。

    v      void CMyClassTester::Add_int_int()

    v      {

    v          {CaseBegin();               //1

    v          int i = 0;                  //2

    v          int j = 0;                  //3

    v          int ret = pObj->Add(i, j);  //4

    v          TestAssert(ret == 0);       //5

    v          CaseEnd(); }                //6

        {CaseBegin();               //1

    v          int i = 10;                 //2

    v          int j = 10;                 //3

    v          int ret = pObj->Add(i, j);  //4

    v          TestAssert(ret == 20);      //5

    v          CaseEnd(); }                //6

    v      }

    这种用例格式在被测试的输入输出非常简单的时候看起来非常的繁琐,但是这种结构是可以适应各种各样的非常复杂的输入和输出。

    下面是一个涉及到成员变量的输入输出的例子,它在用例开头的时候为一个成员变量设定了初始值,后面为两个成员变量判断结果值。

    v      void CMyClassTester::Grow_int

    v      {

    v          {CaseBegin();                    

    v          int years = 1;                               

    v          pObj->mAge = 8;                                

    v          pObj->Grow(years);

    v          TestAssert( pObj->mAge == 9 );

    v          TestAssert( pObj->mPhase == "儿童" );    

    v          CaseEnd(); }                      

    v      }

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

    2009-03-27 10:39:31

    3.1代码错误

    3.1.1分类

    代码错误可以分为两个方面:一个是性能问题,一个是功能错误。

    性能问题可以分为时间性能和空间性能。时间性能就是指代码的执行效率不符合预期。空间性能就是指代码所占用的资源超出预期。由于单元测试面对的测试目标是比较小的代码单元,时间性能和空间性能通常都比较难于衡量,所以单元测试一般不测试性能问题。性能问题的测试通常是在系统测试或者性能测试阶段进行。

    功能错误就是程序没有实现正确的功能。具体来说就是某种输入下没有产生预期的输出。功能错误可以分为有特征错误和无特征错误两种。有特征错误分为行为特征和语法特征。行为特征是在执行的过程当中会产生一些可以捕捉到的行为方面的特征,比如崩溃或者超时。语法特征错误就是在不执行的情况下,通过扫描,自动的或者人工的分析,发现代码当中的某些特征可能潜在一些错误,比如在条件表达式当中使用了赋值操作符,但是编译器是允许这种语法的。语法特征错误通常只是潜在的,不能明确肯定这里有错误,具体来说还可能包含数组越界这一类的。因为在C/C++语言当中数组是具有语法特征的,它是有上下限的,而上下限很多时候是静态的,所以可以通过对比来判断数组是不是有可能越界。这些都算是语法特征错误。总的来说,静态分析可以找出来的错误就叫语法错误。

    单元测试主要测试代码的功能错误。

    3.1.2产生的原因

    从代码单元错误的角度来看,一般有三种原因造成:

    1、  设计错误,就是在设计代码单元的功能时,就已经有错误了。

    2、  录入错误,就是在编码的过程中敲键盘敲错了。

    3、  算法错误,就是在编写代码的时候考虑得不够全面,不够完善,或者是考虑错了。

    算法错误占大多数,其中部分算法错误又占算法错误的大部分。部分算法错误指的是一个程序可能有十种输入,在这十种输入当中有三四种我们是做了正确处理的,可能还有几种是有错误的,甚至有几种是根本没有想到,没有进行处理,从而导致的错误。

    3.1.3特征

    代码错误有一些基本的特性,包括偶然性,随机性和单一性。一般来说开发流程比较好,编码规范比较好,都有助于减少代码错误,还有就是程序员的经验比较丰富,对业务比较熟悉也可以减少代码错误的数量。但是这些只是在程度上,数量上有所减少,要想完全预防错误还是非常困难的。

     

    3.2测试方法

    从测试的主体来说可以分为两类,一类是靠工具自动完成;一类是靠人工完成。

    从测试的方法来说可以分为两种,一种是静态(分析代码),一种是动态(执行代码)。这样我们可以组合出四种测试的方法。

    1、  人工静态分析:通过人工阅读代码来查找错误,一般是交叉阅读,即代码走查。

    2、  自动静态分析:扫描代码中某些错误特征。用工具来扫描,只能发现语法特征错误,因为工具总是要根据某一些预设的语法才能判断代码是否有问题

    3、  自动动态测试:自动生成用例并执行,捕捉某些行为特征。例如异常、崩溃、超时。它只能发现行为特征错误,因为工具再先进也不可能自动了解代码的功能,除非有一些特征被捕抓到,否则它是不可能知道这里是否有错误的。

    4、  人工动态测试:人工设定程序的输入和预期的正确输出,执行程序,并判断实际输出是否符合预期。“人工”,是指用例的输入输出由人工设定,其他工作可以自动化。一般来说人工动态测试称为狭义单元测试。

    我们用一个简单的例子来介绍一下四种测试方法。

    int Add(int a, int b)
    {
        return a-b;
    };

    这是一个加法函数,错误是把“+”号写成“-”。如果是人工静态分析,可能发现这个错误。如果是自动静态分析则不会发现这个错误,因为这里没有什么语法特征。如果是自动动态测试也通常是不会发现的,因为它不会产生崩溃,超时这些可以捕抓得到的行为特征。如果是用人工动态的方法,我们可以输入两个1,接着执行代码,输出不为2的话就让工具自动报错,这样就可以马上发现这里有问题。人工动态测试的基本过程:设定初始状态,接着执行程序(需要一些驱动让程序能够执行起来),然后让工具或测试代码自动判断结果是否正确。在人工动态测试过程中,用例的输入和输出人工设定,其他工作可以自动化。

     

  • 2、为什么要进行单元测试?

    2009-03-27 10:35:12

    单元测试的价值和意义已经得到业界的广泛认同,也有很多文章详细介绍了它的意义,这里只做简单的介绍。

    首先,单元测试可以保证局部代码的质量。只有局部代码的质量得到保证,项目整体质量才能够得到保证。

    其次,单元测试可以降低后期测试、升级维护成本。虽然需要投入一些资源来完成这些测试,但是在后期的测试和升级维护中可以省出很多的成本。在进行单元测试的过程中还会自然实现回归测试的功能,有了回归测试就可以适应频繁变化的需求,也就是项目自然会变得敏捷。在敏捷开发当中的核心就是单元测试,没有单元测试也就没有敏捷可言。有了单元测试,项目自然就会变得敏捷。

    再次,单元测试可以使得开发过程可控。因为单元测试可以保证代码的质量,因此不会使开发到了后期才不断发现代码当中有大量的错误,然后不断的去测试修改。单元测试可以避免项目开发过程出现的这种不可控的情况。

    最后,单元测试可以促进开发。单元测试使大型项目的编码工作也能像编写控制台小程序那样,随时让正在编写的代码单独“跑”起来,还可以让全部可能输入(等价类)都“跑”到,甚至可以随时查看不同输入下的程序行为(什么输入执行什么代码产生什么输出),使编程工作变得轻松和高效。

  • 1、什么是单元测试?

    2009-03-27 10:29:44

    单元测试就是针对代码单元的独立测试。就像电视机工厂对元器件的测试一样,单元测试把源代码单元与其他代码隔离,独立进行测试。单元,一般是指类或函数。如果以类为单元的话,则过于复杂,在实际的应用当中并不合适,所以一般主张以函数为单元。实际上一个函数的测试也可能相当复杂,可能需要十几个用例。

    综上所述,单元测试可以简单理解为:单元测试就是测试各个函数是否有问题。在实际工作中,进行单元测试并不是一件容易的事,能做好函数测试已经相当不错了。

  • 没有输入参数的函数如何设计测试用例?

    2008-10-22 11:36:01

    对于函数测试来说,一个用例,就是设定输入,执行程序,判断输出是否符合预期。
    可能输入包括:参数、需读的成员变量、需读的全局变量、内部输入(调用子函数获得的输入);
    可能输出包括:返回值、输出参数、被写的成员变量、被写的全局变量,内部输出(在程序执行过程中判断的中间输出)、动作(例如需判断程序在某种输入下是否调用了某个函数)。
    简单来说,输入就是程序执行前或执行过程中读取的外部数据,输出就是程序所改写的数据。
    了解了这些,就不会对没有参数、没有返回值如何测试产生疑问了。

    摘自http://bbs.51testing.com/thread-130049-1-1.html

数据统计

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

RSS订阅

Open Toolbar