测就测呗,有啥难的!

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

 

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

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

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

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

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


TAG:

 

评分:0

我来说两句

日历

« 2024-05-10  
   1234
567891011
12131415161718
19202122232425
262728293031 

数据统计

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

RSS订阅

Open Toolbar