实验五 单元测试
实验五 单元测试
一、实验目的
1、掌握单元测试的方法;
2、学习XUnit测试原理及框架;
3、掌握使用测试框架进行单元测试的方法和过程。
二、实验内容与要求
1、了解单元测试的原理与框架
1.1 单元测试原理
单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。对于单元测试中单元的含义,一般来说,要根据实际情况去判定其具体含义,如C语言中单元指一个函数,Java里单元指一个类,图形化的软件中可以指一个窗口或一个菜单等。总的来说,单元就是人为规定的最小的被测功能模块。单元测试是在软件开发过程中要进行的最低级别的测试活动,软件的独立单元将在与程序的其他部分相隔离的情况下进行测试。单元测试是由程序员自己来完成,最终受益的也是程序员自己。可以这么说,程序员有责任编写功能代码,同时也就有责任为自己的代码编写单元测试。执行单元测试,就是为了证明这段代码的行为和我们期望的一致。
单元测试的内容包括:模块接口测试、局部数据结构测试、路径测试、错误处理测试、边界测试。
(1)模块接口测试
模块接口测试是单元测试的基础。只有在数据能正确流入、流出模块的前提下,其他测试才有意义。模块接口测试也是集成测试的重点,这里进行的测试主要是为后面打好基础。测试接口正确与否应该考虑下列因素:
- 输入的实际参数与形式参数的个数是否相同
- 输入的实际参数与形式参数的属性是否匹配
- 输入的实际参数与形式参数的量纲是否一致
- 调用其他模块时所给实际参数的个数是否与被调模块的形参个数相同;
- 调用其他模块时所给实际参数的属性是否与被调模块的形参属性匹配;
- 调用其他模块时所给实际参数的量纲是否与被调模块的形参量纲一致;
- 调用预定义函数时所用参数的个数、属性和次序是否正确;
- 是否存在与当前入口点无关的参数引用;
- 是否修改了只读型参数;
- 对全程变量的定义各模块是否一致;
- 是否把某些约束作为参数传递。
如果模块功能包括外部输入输出,还应该考虑下列因素:
- 文件属性是否正确;
- OPEN/CLOSE语句是否正确;
- 格式说明与输入输出语句是否匹配;
- 缓冲区大小与记录长度是否匹配;
- 文件使用前是否已经打开;
- 是否处理了文件尾;
- 是否处理了输入/输出错误;
- 输出信息中是否有文字性错误。
- 局部数据结构测试;
- 边界条件测试;
- 模块中所有独立执行通路测试;
(2)局部数据结构测试
检查局部数据结构是为了保证临时存储在模块内的数据在程序执行过程中完整、正确,局部功能是整个功能运行的基础。重点是一些函数是否正确执行,内部是否运行正确。局部数据结构往往是错误的根源,应仔细设计测试用例,力求发现下面几类错误:
- 不合适或不相容的类型说明;
- 变量无初值;
- 变量初始化或省缺值有错;
- 不正确的变量名(拼错或不正确地截断);
- 出现上溢、下溢和地址异常。
(3)边界条件测试
边界条件测试是单元测试中最重要的一项任务。众所周知,软件经常在边界上失效,采用边界值分析技术,针对边界值及其左、右设计测试用例,很有可能发现新的错误。边界条件测试是一项基础测试,也是后面系统测试中的功能测试的重点,边界测试执行的较好,可以大大提高程序健壮性。
(4)独立路径测试
在模块中应对每一条独立执行路径进行测试,单元测试的基本任务是保证模块中每条语句至少执行一次。测试目的主要是为了发现因错误计算、不正确的比较和不适当的控制流造成的错误。具体做法就是程序员逐条调试语句。常见的错误包括:
- 误解或用错了算符优先级;
- 混合类型运算;
- 变量初值错;
- 精度不够;
- 表达式符号错。
(5)错误处理测试
检查模块的错误处理功能是否包含有错误或缺陷。例如,是否拒绝不合理的输入;出错的描述是否难以理解、是否对错误定位有误、是否出错原因报告有误、是否对错误条件的处理不正确;在对错误处理之前错误条件是否已经引起系统的干预等。
通常单元测试在编码阶段进行。在源程序代码编制完成,经过评审和验证,确认没有语法错误之后,就开始进行单元测试的测试用例设计。利用设计文档,设计可以验证程序功能、找出程序错误的多个测试用例。对于每一组输入,应有预期的正确结果。
1.2 测试框架
xUnit是各种代码驱动测试框架的统称,这些框架可以测试 软件的不同内容(单元),比如函数和类。xUnit框架的主要优点是,它提供了一个自动化测试的解决方案。可以避免多次编写重复的测试代码。
底层是xUnit的framwork,xUnit的类库,提供了对外的功能方法、工具类、api等
TestCase(具体的测试用例)去使用framwork
TestCase执行后会有TestResult
使用TestSuite控制TestCase的组合
TestRunner执行器,负责执行case
TestListener过程监听,监听case成功失败以及数据结果,输出到结果报告中
Unit测试框架包括四个要素:
(1) 测试目标(对象)
一组认定被测对象或被测程序单元测试成功的预定条件或预期结果的设定。Fixture就是被测试的目标,可以是一个函数、一组对象或一个对象。 测试人员在测试前应了解被测试的对象的功能或行为。
(2)测试集
测试集是一组测试用例,这些测试用例要求有相同的测试Fixture,以保证这些测试不会出现管理上的混乱。
(3)测试执行
单个单元测试的执行可以按下面的方式进行:
第一步 编写 setUp() 函数,目的是:建立针对被测试单元的独立测试环境;举个例子,这可能包含创建临时或代理的数据库、目录,再或者启动一个服务器进程。
第二步 编写所有测试用例的测试体或者测试程序;
第三步 编写tearDown()函数,目的是:无论测试成功还是失败,都将环境进行清理,以免影响后续的测试;
(4)断言
断言实际上就是验证被测程序在测试中的行为或状态的一个函数或者宏。断言的失败会引发异常,终止测试的执行。
1.3 面向特定语言的,基于xUnit框架的自动化测试框架
- Junit : 主要测试用Java语言编写的代码
- CPPunit:主要测试用C++语言编写的代码
- unittest , PyUnit:主要测试用python语言编写的代码
- MiniUnit: 主要用于测试C语言编写的代码
2、结对编程的小组采用测试框架 对自己“结对编程”实验的程序模块(类)进行单元测试,提交单元测试报告:
测试报告包括以下内容:
- 源码
- 测试用例设计 (结合单元测试的内容和模块功能设计测试用例)
- 选择的测试框架介绍、安装过程
- 测试代码
- 测试结果与分析
3、push 测试报告和测试代码到各自的github仓库
4、提交博客报告
三、实验过程
1. 程序源码
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <Windows.h> #include <time.h> #include <conio.h> // 定义地图的大小 #define M 20 #define N 20 // 全局变量 int Map1[M][N] = {0}; int Map2[M][N] = {0}; int thisFlag = 1; // 函数声明 int ReadMap(); void Print(int thisMap[M][N],int lastMap[M][N]); void InitPrintMap(int thisMap[M][N]); void Run(); void Calculate(int nowMap[M][N],int lastMap[M][N]); void RenewMap(); void Pos(int x, int y); // 读取初始地图 int ReadMap() { FILE *fp; // 利用指针打开事先准备好的文本文件 int i = 0,j = 0; char ch = '\0'; // 先判断地图文件是否存在,存在则继续,否则直接返回-1 if(access("LifeGameMap.txt",0)) { printf("地图文件不存在\n"); return -1; } fp = fopen("LifeGameMap.txt","r"); for(i = 0;i < M;i++) { // 读完一整行 for(j = 0;j < N;j++) { fscanf(fp,"%c",&ch); if('*' != ch) { Map1[i][j] = 0; // nowmap[][] Map2[i][j] = 0; // lastmap[][] } else { // 设置为1,方便后期计算周围活细胞个数(直接累加就行) Map1[i][j] = 1; Map2[i][j] = 1; } } // 一行最后有一个换行符 fscanf(fp,"%c",&ch); } return 0; } // 初始化地图 void InitShowMap(int thisMap[M][N]) { int i = 0,j = 0; for(i = 0;i < M;i++) { for(j = 0;j < N;j++) { if(0 == thisMap[i][j]) printf("□"); else printf("■"); } printf("\n"); } printf("请按Y(y)继续进化:"); } // 根据规则计算新地图 void Calculate(int nowMap[M][N],int lastMap[M][N]) { int i = 0,j = 0,count = 0; for(i = 0;i < M;i++) { for(j = 0;j < N;j++) { count = 0; // 特殊情况(第一排、最后一排、左边中间、右边中间、正中间)特殊计算,否则计算四周 if(0 == i) { // 在第一排 if(0 == j) { count = nowMap[i][j+1]+nowMap[i+1][j]+nowMap[i+1][j+1]; } else if(j == N-1) { count = nowMap[i][j-1]+nowMap[i+1][j]+nowMap[i+1][j-1]; } else { count = nowMap[i][j-1]+nowMap[i+1][j-1]+nowMap[i][j+1] +nowMap[i+1][j]+nowMap[i+1][j+1]; } } else if(M-1 == i) { if(0 == j) { count = nowMap[i][j+1]+nowMap[i-1][j]+nowMap[i-1][j+1]; } else if(j == N-1) { count = nowMap[i][j-1]+nowMap[i-1][j]+nowMap[i-1][j-1]; } else { count = nowMap[i][j-1]+nowMap[i-1][j-1]+nowMap[i][j+1] +nowMap[i-1][j]+nowMap[i-1][j+1]; } } else if(0 == j) // 左边中间的情况 count = nowMap[i-1][j]+nowMap[i-1][j+1]+nowMap[i][j+1] +nowMap[i+1][j+1]+nowMap[i+1][j]; else if(N-1 == j) count = nowMap[i-1][j]+nowMap[i-1][j-1]+nowMap[i][j-1] +nowMap[i+1][j-1]+nowMap[i+1][j]; else count = nowMap[i-1][j]+nowMap[i-1][j-1]+nowMap[i][j-1] +nowMap[i+1][j-1]+nowMap[i+1][j]+nowMap[i-1][j+1] +nowMap[i][j+1]+nowMap[i+1][j+1]; // 根据count的结果来判断新的地图中细胞的生死 if(3 == count) lastMap[i][j] = 1; else if(2 == count) lastMap[i][j] = nowMap[i][j]; else lastMap[i][j] = 0; } } } // 遍历当前的地图数组,依次计算每个格子的后一个状态,并更新新的地图 void RenewMap() { if(1 == thisFlag)//nowmap { Calculate(Map1,Map2); thisFlag = 2; } else { Calculate(Map2,Map1); thisFlag = 1; } } // 保持细胞的运行 void Run() { int i = 0,j = 0; if(-1 == ReadMap()) return; InitShowMap(Map1); while(1) { switch(getch()) { case 'y':RenewMap(); break; case 27:return ; } // Sleep(500); if(1 == thisFlag) Print(Map2,Map1); else Print(Map1,Map2); } } void Pos(int x, int y) // 设置光标位置 { // 要注意这里的x和y与我们数组的x和y是反的 COORD pos; HANDLE hOutput; pos.X = x; pos.Y = y; hOutput = GetStdHandle(STD_OUTPUT_HANDLE); // 返回标准的输入、输出或错误的设备的句柄,也就是获得输入、输出/错误的屏幕缓冲区的句柄 SetConsoleCursorPosition(hOutput, pos); } int main(void) { Run(); system("pause"); return 0; } // 打印当前地图 void Print(int thisMap[M][N],int lastMap[M][N]) { int i = 0,j = 0; for(i = 0;i < M;i++) { for(j = 0;j < N;j++) { // 不相等,所以需要改变输出 if(thisMap[i][j] != lastMap[i][j]) { Pos(2*j,i); // Sleep(500); if(0 == lastMap[i][j]) printf("□"); else printf("■"); } } } Pos(N,M); }
2. 选择的测试框架介绍和安装过程
生命游戏的代码是利用C语言程序编写的,基于C语言,有许多可以使用的单元测试框架
最后选择了VS软件自带的单元测试框架
(1)打开VS2019,新建项目,可以看到右侧有许多待选择的项目类型,查找出自己需要的类型;
(2)新建项目,项目名为Test
(3)进入新建项目,添加文件test.cpp,注意将待测试的问价编译好加入Test目录下;
(4)补充代码,点击运行开始进行调用测试,跳转出命令符界面,显示测试结果;
(5)如果待测文件有错误,显示错误的结果;
(6) 提交到github仓库
三、实验小结
1、本次实验我尝试了很多针对C语言进行单元测试的方法,但是真的很多都不能使用,可能是之前编写的生命游戏的代码有问题,当时是利用外部的txt文件建立的地图,导致在VS中进行测试的时候出现了大量的编译错误;
2、在使用VS之前,我尝试了几种C语言代码单元测试工具,第一个就是Vistual Unit 4,这是一种可视化、自动化、标准化、高效率的C/C++单元测试工具,功能强大并且易学易用。
但是这个软件安装成功时只能利用演示版,观看学习软件的功能,除非连接License,否则不能对自己编写的代码进行编译,但是界面,功能真的都很棒。
3、除了Vistual Unit 4,我还尝试了miniunit框架,但是将miniunit的头文件加入到待测程序中后,编译出错,百度查找解决方法也没有找到;
4、本次实验并没有完成对之前编写的生命游戏的C语言代码的单元测试,但是从网上查询的那么多资料,我深深的体会到了单元测试的重要性,测试是保证软件产品质量的基本手段,将代码单元与其他代码隔离进行测试,就相当于电视机工厂对元器件的测试。软件单元测试甚至可能比电视机的元器件测试有更高必要性和价值,因为代码单元是人工编写的,"故障率"更高,导致在集成后查找代码错误的单位成本也高得多。我也体会到单元测试的不易,甚至感觉比编写源程序还要难操作,一个测试效果好测试效率高的单元测试工具真的很重要。
四、思考题
比较以下二个工匠的做法,你认为哪种好?结合编码和单元测试,谈谈你的认识。
我认为工匠二的做法更好,虽然说单元测试的理念是将代码单元与其他代码隔离进行测试,即分模块进行测试,但若所分模块很小,就不容易找出问题,即使问题找出并解决后,多个模块在一起可能还会产生新的错误,而且工匠一的做法太过于浪费时间了。选择工匠二可以提高测试软件代码的速度,提高软件的开发效率。