单元测试概述
单元测试的定义
单元测试是指,对软件中的最小可测试单元在与程序其他部分相隔离的情况下进行检查和验证的工作。
单元测试的测试对象:
- 在结构化编程中,单元测试对象指:函数
- 在面向对象编程中,单元测试对象指:类。但以类作为测试对象,复杂性高,可操作性差,所以仍然主张以类中的方法作为测试对象,也可以用一个测试类来组织某个类的所有测试函数。
单元测试的技术:主要采用白盒测试方法,辅之以黑盒测试方法设计测试用例。
单元测试的内容
- 模块接口测试:模块接口测试是单元测试的基础。
- 局部数据结构测试:保证临时存储在模块内的数据在程序执行过程中完整、正确,局部功能是整个功能运行的基础。
- 独立路径测试
- 错误处理测试
- 边界测试
(下一篇中将详细写)
单元测试的过程:计划,设计,执行,评估
单元测试的人员:开发人员
单元测试属于最严格的软件测试手段
单元测试的对象是:代码。因此为了做好单元测试,我们首先要了解代码的基本特征和产生错误的原因。
在开发中不论我们使用那种开发语言,都会使用到条件分支,循环处理和函数调用等基本的逻辑控制,这些逻辑控制的作用是对数据进行正确的分类处理。因此分类的遗漏,分类错误,分类之后处理逻辑错误都会使代码产生错误。
设计单元测试用例
知道了上边的基本内容之后,我们来看怎样设计一个单元测试的用例。单元测试用例是什么呢?单元测试的用例是一个“输入数据”和“预计输出”的集合。接下来我们分别了解单元测试的“输入数据”类型和“预计输出”类型
单元测试用例的“输入数据”类型
被测试函数的输入参数
这个很好理解,被测试函数如下:
void add(int a,int b)
{
...
}
那么函数输入参数 a 和 b 的不同取值以及取值的组合就构成了单元测试的输入数据。
被测试函数内部需要读取的全局静态变量
被测试函数如下:
bool condition= true;//condition为全局静态变量
void Function_1(int a)
{
...
if(condition == true)
{
FuncA();
}
else
{
FuncB();
}
...
}
Function_1函数根据全局静态变量condition的不同取值执行不同的代码分支。在测试的过程中,我们为了能够覆盖这两个代码分支,必须让condition取到不同的值。因此,全局静态变量condition是被测函数的输入数据。
被测试函数内部需要读取的类成员变量
被测试函数如下:
class someClass{
...
bool C_condition = true;//类成员变量
...
void Function_2(int a)
{
...
if(condition == true)
{
FuncA();
}
else
{
FuncB();
}
...
}
...
}
Function_2函数根据类成员变量C_condition的不同取值执行不同的代码分支。在测试的过程中,我们为了能够覆盖这两个代码分支,必须让C_condition取到不同的值。因此,类成员变量C_condition是被测函数的输入数据。
函数内部调用子函数获得数据
void function_3(int a)
{
bool toggle = Fun_son(a);//Fun_son()函数为子函数
if(toggle == true)
{
FuncA();
}
else
{
FuncB();
}
}
被测函数function_3调用子函数Fun_son(),并把Fun_son()函数的返回值赋值给内部变量toggle,然后根据toggle取值的不同执行不同的代码分支。子函数Fun_son()为function_3的内部变量toggle提供了数据,因此被测试函数内部调用子函数获取的数据也是单元测试的输入数据。
这里要注意的是int a并不是被测函数的单元测试输入数据。因为int a在此段代码中仅仅作为子函数Fun_son()的输入参数使用,我们直接对 Fun_son(a) 打桩(后面介绍什么是打桩),用桩代码来控制函数Fun_son()函数返回的是 true 还是 false,这样int a便没有任何作用,不是单元测试的输入数据。
函数内部调用子函数改写的数据
bool toggle=true;//全局变量
void function_3(int a)
{
toggle = Fun_son(a);//Fun_son()函数为子函数
if(toggle == true)
{
FuncA();
}
else
{
FuncB();
}
}
这里全局变量toggle是被调用子函数改写的数据,通过上面的学习,不难知道全局变量toggle是被测函数单元测试的输入数据。当然,这里被调用子函数改写的数据也可以是类成员变量。
嵌入式系统中,在中断调用中改写的数据
嵌入式系统中,在中断调用中改写的数据有时候也会成为被测函数的输入参数,这和“函数内部调用子函数改写的数据也是单元测试中的输入参数”类似,在某些中断事件发生并执行中断函数时,中断函数很可能会改写某个寄存器的值,但是被测函数的后续代码还要基于这个寄存器的值进行分支判断,那么这个被中断调用改写的数据也就成了被测函数的输入参数。
单元测试用例的“预计输出”类型
被测函数的返回值
int add(int a,int b)
{
int sum = 0;
sum = a+b;
return sum;
}
这里的返回值sum,便是一个预期输出。
被测函数的输出参数
void add(int a, int b,int *sum)
{
*sum = a + b;
}
void main()
{
int a, b,sum;
a = 2;
b = 24;
add(a, b, &sum);
printf("sum = %d \n", sum);
}
被测函数 add 的参数有: a 和 b 两个输入参数,和sum 指针。在main()函数中我们通过访问 sum 指向的空间来获得sum在被测试函数add()中所赋的值,相当于你把函数内部的值输出到了函数外,因此sum 就是我们的“预期输出”。
被测函数所改写的成员变量和全局变量
如果单元测试用例需要写断言来验证结果,那么这些被改写的类的成员变量和全局变量就是 assert 的对象。
被测函数中进行的文件更新、数据库更新、消息队列更新等
如:你的代码对一个文本文件中的数据进行了排序,那么排序之后的文本文件也是一个“预期输出”。
总结:“输入参数”不仅仅是被测函数的输入参数;“预计输出”也不仅仅是函数的返回值。接下来我们来看单元测试中常用到的驱动代码,桩代码和Mock代码是什么。
驱动代码,桩代码和 Mock 代码
驱动代码:是用来调用被测函数的。它接收测试数据,把接收的数据传给被测函数,最后再输出测试结果。
桩代码:是用来代替被测函数调用的子函数的。如:函数 A 的内部实现中调用了一个尚未实现的函数 B,为了对函数 A 的逻辑进行测试,那么就需要模拟一个函数 B,这个模拟的函数 B 的实现就是所谓的桩代码。桩代码根据起作用可分为两种:实现隔离和补齐的桩函数 和 实现控制功能的桩函数
编写桩代码需要遵守的三个原则:
- 桩函数要具有与原函数完全相同的原形,仅仅是内部实现不同,这样测试代码才能正确链接到桩函数;
- 用于实现隔离和补齐的桩函数比较简单,只需保持原函数的声明,加一个空的实现,目的是通过编译链接;
- 实现控制功能的桩函数是应用最广泛的,要根据测试用例的需要,输出合适的数据作为被测函数的内部输入。
Mock 代码:和桩代码非常类似,都是用来代替真实代码的临时代码,起到隔离和补齐的作用。与桩代码相比较:我们的关注点是 Mock 方法有没有被调用,以什么样的参数被调用,被调用的次数,以及多个 Mock 函数的先后调用顺序。但桩代码并不关心这些。
评估单元测试
我们主要从以下几个方面进行评估:
测试完备性评估
检测测试过程中是否已执行了所有测试用例,对新的测试用例是否已及时更新测试方案等
代码覆盖率评估
根据代码覆盖率工具提供的语句覆盖率报告,检测是否达到方案中的要求,大所属情况下,要求语句覆盖率达到100%。若没有达到要求,要对覆盖率进行分析。主要从这几个方面考虑:不可能的路径或条件,不可达的或冗余的代码和不充分的测试用例。
覆盖角度评估
测试应做到以下覆盖:功能覆盖,输入域覆盖,输出域覆盖,函数交互覆盖和代码执行覆盖。