1、自然输入
自然输入是指对底层函数的正常调用即可获得的内部输入。代码一中Compare()函数内,int a1 = GetArea(r);可以自然取得外接正方形的面积。如果外接正方形面积a1要得到某个预期的值,要传递合适的半径r,半径r称为间接输入。间接输入需根据自然输入及底层函数的功能来倒推,要获得符合预期的自然输入有三个条件:一是底层函数存在,二是底层函数正确,三是间接输入正确。很多时候,自然输入很简单,而间接输入很复杂,这就是难于初始化。
2、不可控
是指调用实际代码,但实际代码的输出难于控制,无法产生测试需要的指定值。例如,底层函数返回一个随机数,就是不可控。在实际项目中,不可控是很常见的,下面的代码是空调控制程序中的一个函数:
extern int GetTemperature(int *pTemperature); //取环境温度
//取环境温度,这是测试难点 if(!success) return 0;
//计算温度差,gExpectTemperature是全局变量 if(pWorkTime == 0) return 0;
//为了简化问题,这里假设温差一度,需运行60秒 |
测试的难点在于success = GetTemperature(&temperature);,这行代码调用GetTemperature()取环境温度,如果操作成功,success等于1,操作不成功,success等于0;取得的环境温度保存在局部变量int temperature中。假设在实际环境中测试,调用的都是实际代码。首先要设定预期的温度gExpectTemperature,例如设为25,这是全局变量,容易做到。还要测试各种环境温度下程序的行为,例如,至少要测试25,大于25和小于25三种情况,显然,这是很困难的,真实的环境温度在短时间内很难大幅变化,即使大幅变化,也未必符合测试需求,这就是不可控。
3、失真
失真是打桩造成的,是打桩的必然后果。上面的示例,假如GetTemperature()未实现,或者由于解耦合的目的必须隔离,或者试图解决不可控的问题打桩来代替,桩代码大致是这个样子:
int GetTemperature(int *pTemperature) { return 0; } |
直接返回0,此外什么也不做。调用GetTemperature()后,success总是为0,环境温度temperature未初始化,测试无法进行。
如何解决失真?一种思路是修改桩代码,使它实现一些功能,例如,给每个用例起一个名字,桩代码判断当前用例名并做合适的操作。这种方法比较麻烦,并且只能适应简单情形。一个桩可能被同一函数在同一用例多次调用,每次要求输出不同;一个桩可能被多个函数调用;一个函数又可能调用多个桩;一个函数可能需要十几个用例,随时可能补充和修改用例。要维护用例名和桩行为的对应关系,无疑是一场噩梦
4、难于初始化
前面介绍自然输入时提到:如果圆的外接正方形面积a1要得到某个预期的值,要传递合适的半径r,这是通过外部输入来获得预期的内部输入,即需要倒推外部输入。这个工作常常是很困难的,如果要设定的不是圆的外接正方形的面积,而是圆的面积,就困难得多,例如要求圆的面积为10.00,半径应该是多少?另外,很多时候,为了获得一个简单的内部输入,需要做复杂的初始化工作,请看下面的示例:
/* 功能: 将PERSON对象指针保存到表中,如果名字已存在,则不保存并返回0 参数: pData, 需保存的对象指针 map, 保存对象指针的映射表 返回: 如果加入失败,返回0,否则返回非0值 */ int AddPerson(PERSON *pData, CPersonMap *map) { if(map->Search(&pData->name)) return 0; map->Add(pData); return 1; } |
参数PERSON *pData是结构指针,记录一个“人”的资料,结构PERSON含有一个字符串成员name,记录“人”的名字。参数CpersonMap *map是一个映射表,以名字为key保存PERSON的对象指针。代码很简单,当名字已在表中存在时,直接返回0,否则保存到映射表。测试时要使map->Search(&pData->name)返回true,一般的方法是在表中预先加入相应的数据,这可能很麻烦,如果能直接让map->Search(&pData->name)返回true,既简单又直接。
难于初始化非常常见,尤其在测试比较高层的函数时,很多输入都是间接输入,本身很复杂,但被测函数并不对其直接读写,只是传递给底层函数以获得一个简单的内部输入,如果我们转换思路,不设定间接输入,而是想办法直接设定内部输入,工作量将会大幅减少。
5、静态输入
是指局部静态变量形成的输入。局部静态变量与全局变量一样,每个用例也可能需设定不同的初始值(输入),但却无法外部访问,因此,局部静态变量也会形成内部输入。请看下面的代码:
/* 功能: 游戏程序中用于计算打击效果的代码片断,连续打击时效果随次数递减 参数: reset,为true时重置打击次数 返回: int类型,打击效果 */ int PowerEffect(bool reset) { //打击次数,由于是局部变量,用例中无法访问,形成测试难点 staticint times = 0; if(reset) times = 0; times++; int effect[] = {9, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0}; if(times >= sizeof(effect) / sizeof(effect[0])) return 0; return effect[times]; } |
打击次数times是一个局部静态变量,局部变量无法外部访问,这给测试造成了困难。这个示例中,打击次数只是简单递加,还有可能通过适当排列用例,或插入若干前置调用来控制它的值,但在实际项目中,未必那么简单,因此,静态输入也是一种必须解决的内部输入。
6、中断输入
中断输入常见于嵌入式项目。是指被测函数运行过程中,在某个位置,系统调用了一个中断函数,该函数修改了某个全局变量,如果被修改的全局变量对被测函数的功能逻辑造成影响,那么测试时也必须考虑。
7、总结
前面列出了内部输入的六种情形,后五种是影响单元测试能否顺利实施的关键难点。有趣的是,内部输入的问题通常会被归结为“代码可测性差”,解决办法是改良代码提高可测性,这是不现实的,这些问题大量存在并且多数不可能消除。单元测试方法或工具,如果无法解决内部输入问题,就无法适应实际项目的测试,这不是代码可测性问题,而是方法或工具的可用性问题。解决内部输入的方法主要有两种:编写桩代码,底层模拟,下篇文章会进一步介绍。