实验五 单元测试
一、实验目的
1)掌握单元测试的方法
2) 学习XUnit测试原理及框架;
3)掌握使用测试框架进行单元测试的方法和过程。
二、实验内容与要求
1、了解单元测试的原理与框架
1.1 单元测试原理
单元测试,是指对软件中的最小可测试单元进行检查和验证。对于单元测试中单元的含义,一般来说,要根据实际情况去判定其具体含义,如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语言编写的代码
三、实验过程
1)源码
- data层存放细胞数组类
- service层存放算法逻辑
- lifeGame应用层主要是界面的实现和启动
以data层的类作为驱动模块,lifeGame的类作为桩模块,测试GameService.java,而data和lifeGame中的类都采用静态测试的方法。
//GameService.java 1 package service; import data.CellArray; import java.util.Random; import javax.swing.JFrame; import data.CellState; public class GameService { public static int[] temp= {-1,0,1}; //temp是用来移动坐标的:(x,y) //初始化矩阵中所有细胞,随机赋予它们的生死 public static CellArray initMap(int row,int col ) { CellArray cells=new CellArray(row,col); for(int i=0;i<row;i++) { for(int j=0;j<col;j++) { Random r=new Random(); int a=r.nextInt(4); //在0-3之间随机选一个数 if(a==1) { cells.setCell(i,j,CellState.LIVE.getValue()); } else { cells.setCell(i,j,CellState.DEAD.getValue()); } } } return cells; } //判断细胞下一代的生死 public static CellArray generate(CellArray cells) { CellArray nextCells=new CellArray(cells.getRow(),cells.getCol()); for(int i=0;i<nextCells.getRow();i++) for(int j=0;j<nextCells.getCol();j++) { int count=countNumber(cells,i,j); if(count==3) { nextCells.setCell(i, j, CellState.LIVE.getValue()); //活邻居为三 活 } else if(count==2&&cells.getCell(i, j)==CellState.LIVE.getValue()) { //活邻居为二且自己本来就活着 活 nextCells.setCell(i, j, CellState.LIVE.getValue()); } else { nextCells.setCell(i, j, CellState.DEAD.getValue()); //其他情况 死 } } return nextCells; } //统计矩阵中每个细胞活邻居的个数 public static int countNumber(CellArray cells,int x,int y) { int count = 0; for (int i = 0; i < 3; ++i) { for (int j = 0; j < 3; ++j) { if (CellState.LIVE.getValue() == cells.getCell(x + temp[i], y + temp[j])) { //统计(x,y)周围8个细胞中活细胞的数目 ++count; } } } if (CellState.LIVE.getValue() == cells.getCell(x, y)) { //如果(x,y)本身是活的,需要减掉一个 --count; } return count; } }
2)测试用例设计 (结合单元测试的内容和模块功能设计测试用例)
initMap()函数 初始一个方阵,遍历每个方格,每个方格都配有一个随机数a,a属于[0,3],若a=1,该方格中的细胞初始为活细胞,其他情况都是死细胞。
用例描述 | 用例参数 | 期望结果 |
验证初始不为NULL | (30,30) | 一个30*30的矩阵,里面每个方格随机赋值1,0 |
验证矩阵的随机性 | (30,30) | 再次初始化一个30*30的矩阵,与前一个矩阵对比,结果应为不一样 |
countNumber()函数 统计矩阵中每个细胞活邻居的个数,这里固定矩阵大小为5*5(简化问题),因为矩阵的内容是随机生成的,所以无法预先得知结果,因此无法使用断言,改用输出再计算的方法。
用例描述 | 用例参数 | 期望结果 |
矩阵边界的方格 | (0,0) | - |
矩阵边界的方格 | (0,3) | - |
矩阵中间的方格(周围有8个方格) | (2,2) | - |
过界方格 | (-1,-1) | - |
过界方格 | (5,5) | - |
generate()函数 判断下一代细胞的生死,固定矩阵大小为5*5,同样矩阵内容随机生成,无法提前得知结果,采用输出再计算的方法。
用例描述 | 用例参数 | 期望结果 |
边界上 | (1,1) | - |
中间 | (2,2) | - |
边界上 | (6,6) | - |
过界 | (7,7) | - |
3)选择的测试框架介绍、安装过程
Eclipse 集成了 JUnit,可以非常方便地编写 Test Case。Eclipse 自带了一个 JUnit 插件,不用安装就可以在项目中测试相关的类,并且可以调试测试用例和被测类。
单元测试,注意需要遵循一下规则:
- 每一个测试方法上使用@Test进行修饰
- 每一个测试方法必须使用public void 进行修饰
- 每一个测试方法不能携带参数
- 测试代码和源代码在两个不同的项目路径下
- 测试类的包应该和被测试类保持一致
- 测试单元中的每个方法必须可以独立测试
下面开始使用:
选中需要测试的项目右击选择属性,在弹出的属性窗口中,首先在左边选择“Java Build Path”,然后到右上选择“Libraries” 标签,之后在最右边点击“Add Library…”按钮。
选中需要测试的类右击选择new->JUnit Test Case
勾中要测试的方法,完成。
4 )测试代码
InitMap()函数
package service; import static org.junit.Assert.*; import org.junit.After; import org.junit.Before; import org.junit.Test; import data.CellArray; import junit.framework.Assert; public class GameServiceTest { CellArray new_Cells=GameService.initMap(20, 20); @Before public void setUp() throws Exception { // setUp() 方法会在每个测试方法执行前被调用 } @After public void tearDown() throws Exception{ // tearDown() 方法则会在每一个测试方法执行后被调用 } @Test public void testInitMap1() { for(int i=0;i<30;i++) { for(int j=0;j<30;j++) { System.out.printf("%d ",new_Cells.getCell(i, j)); } System.out.println(); } } @Test public void testInitMap2() { CellArray new2_Cells=GameService.initMap(20, 20); Assert.assertNotSame(new2_Cells, new_Cells); } }
countNumber()函数
package service; import static org.junit.Assert.*; import org.junit.After; import org.junit.Before; import org.junit.Test; import data.CellArray; public class GameServiceTest { CellArray new_Cells=GameService.initMap(5, 5); @Before public void setUp() throws Exception { // setUp() 方法会在每个测试方法执行前被调用 } @After public void tearDown() throws Exception{ // tearDown() 方法则会在每一个测试方法执行后被调用 } @Test public void testInitMap1() { for(int i=0;i<5;i++) { for(int j=0;j<5;j++) { System.out.printf("%d ",new_Cells.getCell(i, j)); } System.out.println(); } } @Test public void testCountNumber() { System.out.printf("%d\n",GameService.countNumber(new_Cells, 0, 0)); System.out.printf("%d\n",GameService.countNumber(new_Cells, 0, 3)); System.out.printf("%d\n",GameService.countNumber(new_Cells, 2, 2)); System.out.printf("%d\n",GameService.countNumber(new_Cells, -1,-1)); System.out.printf("%d\n",GameService.countNumber(new_Cells, 5,5)); } }
generate()函数
package service; import static org.junit.Assert.*; import org.junit.After; import org.junit.Before; import org.junit.Test; import data.CellArray; public class GameServiceTest { @Before public void setUp() throws Exception { // setUp() 方法会在每个测试方法执行前被调用 } @After public void tearDown() throws Exception{ // tearDown() 方法则会在每一个测试方法执行后被调用 } @Test public void testGenerate() { CellArray new_Cells=GameService.initMap(7, 7); for(int i=0;i<7;i++) { for(int j=0;j<7;j++) { System.out.printf("%d ",new_Cells.getCell(i, j)); } System.out.println(); } System.out.println(); CellArray next_Cells=GameService.generate(new_Cells); System.out.printf("%d (1,1)\n", next_Cells.getCell(1, 1)); System.out.printf("%d (2,2)\n", next_Cells.getCell(2, 2)); System.out.printf("%d (6,6)\n", next_Cells.getCell(6, 6)); System.out.printf("%d (7,7)\n", next_Cells.getCell(7, 7)); } }
5)测试结果与分析
InitMap()函数 符合预期,测试通过
countNumber()函数 测试通过,结果不符合预期
分析之后,猜测是因为边界不清晰,初始7*7矩阵为0,随机填充6*6(1<=x,y<=5)。测试通过结果正确。
generate()函数 测试通过结果符合预期
6)push测试报告和测试代码到各自的github仓库
思考题
比较以下二个工匠的做法,你认为哪种好?结合编码和单元测试,谈谈你的认识。
答:显然工匠一的做法更好,每砌一块砖都要与水平线比较看似繁琐多余,但可以免除后面一系列的麻烦,每一步都走的稳妥,工匠二看上去很快,但是不可能没有误差,每一步都只是小小的误差,等到做完误差就大的接收不了了,这个时候想要修改就只能推倒重来,如此可能比工匠一还要慢。对于编码和单元测试,其中的单元就是工匠手中的砖块,做好单元测试是非常有必要的。
实验小结
本次实验花了不少时间,代码又重新梳理了一边,发现有些地方之前理解的不对(真心打脸啊),对于测试用例的选用也思考了很久,希望能用最少的例子得到最好的结果,也许最后呈现出来的东西没有达到预期,但是也很心满意足了,针对此次单元测试,我得到最深的感想其实是很多测试在编程过程中就测试完毕是最好最快可以免除许多后顾之忧的方法,长久以来编写程序总是抱着“还没完成,运行啥?”的想法,比如这次发现的错误,如果在编写过程中测试的话就会立刻发现,从而减少那些不必要的麻烦。这也就是为什么单元测试需要程序员自己操作的缘故了吧!