自动动态方法并不能理解代码逻辑,所以仅仅被用于发现异常、崩溃和超时这类“有特征”的错误,而对于代码逻辑功能的测试,主要还是要依靠人工动态方法。
人工动态方法
人工动态测试方式,是最常用的代码级测试方法,也是我们在进行单元测试时采用的方法。
单元测试用例“输入参数”的复杂性
如果你认为单元测试的输入参数只有被测函数的输入参数的话,那你就把事情想得过于简单了。
常见的单元测试输入数据有哪些?
第一,被测试函数的输入参数
第二,被测试函数内部需要读取的全局静态变量
如果被测函数内部使用了该函数作用域以外的变量,那么这个变量也是被测函数的输入参数。
bool someGlobalVariable = true;
void Func_SUT(int a)
{
...
if(someGlobalVariable == true)
{
FuncA();
}
else
{
FuncB();
}
...
}
在这段代码中,单元测试的输入参数不仅包括 Func_SUT 函数的输入参数 a,还包括全局变量 someGlobalVariable。
第三,被测试函数内部需要读取的类成员变量
class someClass{
...
bool someClassVariable = true;
...
void Func_SUT(int a)
{
...
if(someClassVariable == true)
{
FuncA();
}
else
{
FuncB();
}
...
}
...
}
单元测试想要覆盖这两个分支,就必须提供 someClassVariable 的不同取值,所以 someClassVariable 对于被测函数 Func_SUT 来说也是输入参数。
第四,函数内部调用子函数获得的数据
void Func_SUT(int a)
{
bool toggle = FuncX(a);
if(toggle == true)
{
FuncA();
}
else
{
FuncB();
}
}
函数 FuncX 的调用为被测函数 Func_SUT 提供了数据,也就是这里的变量 toggle,后续代码逻辑会根据变量 toggle 的取值执行不同的分支。所以,从这个角度来看,被测函数内部调用子函数获得的数据也是单元测试的输入参数。
有些情况下“间接输入参数”反而不是输入参数。
被测函数 Func_SUT 的输入参数 a,在内部实现上只是传递给了内部调用的函数 FuncX,而并没有在其他地方被使用,我们把这类用于传递给子函数的输入参数称为“间接输入参数”。
通过变量 a 的取值很难控制 FuncX 的返回值(也就是说,当通过间接输入参数的取值去控制内部调用函数的取值,以达到控制代码内部执行路径比较困难)时,我们会直接对 FuncX(a) 打桩,用桩代码来控制函数 FuncX 返回的是 true 还是 false。
原本的变量 a 其实就没有任何作用了。那么,此时变量 a 虽然是被测函数的输入参数,但却并不是单元测试的输入参数。
第五,函数内部调用子函数改写的数据
当被测函数内部调用的子函数改写了全局变量或者类的成员变量,而这个被改写的全局变量或者类的成员变量又会在被测函数内部被使用,那么“函数内部调用子函数改写的数据”也就成为了被测函数的输入参数了。
第六,嵌入式系统中,在中断调用中改写的数据
嵌入式系统中,在中断调用中改写的数据有时候也会成为被测函数的输入参数,这和“函数内部调用子函数改写的数据也是单元测试中的输入参数”类似,在某些中断事件发生并执行中断函数时,中断函数很可能会改写某个寄存器的值,但是被测函数的后续代码还要基于这个寄存器的值进行分支判断,那么这个被中断调用改写的数据也就成了被测函数的输入参数。
单元测试用例“预期输出”的复杂性
通常来讲,“预期输出”应该包括被测函数执行完成后所改写的所有数据,主要包括:被测函数的返回值,被测函数的输出参数,被测函数所改写的成员变量和全局变量,被测函数中进行的文件更新、数据库更新、消息队列更新等。
关联依赖的代码不可用
假定函数 A 调用了函数 B,而函数 B 由其他开发团队编写,且未实现,那么我们就可以用桩函数来代替函数 B,使函数 A 能够编译链接,并运行测试。
桩函数要具有与原函数完全相同的原形,仅仅是内部实现不同,这样测试代码才能正确链接到桩函数。一般来讲桩函数主要有两个作用,一个是隔离和补齐,另一个是实现被测函数的逻辑控制。
自动动态方法
自动动态方法是,基于代码自动生成边界测试用例并执行来捕捉潜在的异常、崩溃和超时的测试方法。
自动动态方法的重点是:如何实现边界测试用例的自动生成。
解决这个问题最简单直接的方法是,根据被测函数的输入参数生成可能的边界值。
具体来讲,任何数据类型都有自己的典型值和边界值,我们可以预先为它们设定好典型值和边界值,然后组合就可以生成了。
比如,函数 int func(int a, char *s),就可以按下面的三步来生成测试用例集。
-
定义各种数据类型的典型值和边界值。 比如,int 类型可以定义一些值,如 int 的最小值、int 的最大值、0、1、-1 等;char* 类型也可以定义一些值,比如“”、“abcde”、“非英文字符串”等。
-
根据被测函数的原形,生成测试用例代码模板,比如下面这段伪代码:
try{
int a= @a@;
char *s = @s@;
int ret = func(a, s);
}
catch{
throw exception();
}
- 将参数 @a@和 @s@的各种取值循环组合,分别替换模板中的相应内容,即可生成用例集。
由于该方法不可能自动了解代码所要实现的功能逻辑,所以不会验证“预期输出”,而是通过 try…catch 来观察是否会引发代码的异常、崩溃和超时等具有边界特征的错误。
来源于 极客时间 茹炳晟 软件测试52讲