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

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

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

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

比如说在下面这个例子:

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始终未初始化。这样子无论输入是什么,后边的这种路径是一样的。这样会造成测试没有办法进行下去,这是失真。解决失真的一种办法是自己去修改桩代码,但在实际工作中自己去修改桩代码也是很难的。因为我们的桩代码它要实现什么样的功能才能代替正式的代码,这太复杂了,而且不同的用例桩代码应该是做不同的动作的。比如说不同的用例当中,a和b的值应该是不一样的,你很难在桩代码当中去实现这种控制。失真是必须要解决的一个难题,否则的话测试是很难实现预期的效果的。

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

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