软件工程实验五---单元测试
软件工程实验五——单元测试
一、实验目的
- 掌握单元测试的方法
- 学习XUnit测试原理及框架
- 掌握使用测试框架进行单元测试的方法和过程
二、实验内容与要求
了解单元测
单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。对于单元测试中单元的含义,一般来说,要根据实际情况去判定其具体含义,如C语言中单元指一个函数,Java里单元指一个类,图形化的软件中可以指一个窗口或一个菜单等。总的来说,单元就是人为规定的最小的被测功能模块。单元测试是在软件开发过程中要进行的最低级别的测试活动,软件的独立单元将在与程序的其他部分相隔离的情况下进行测试。单元测试是由程序员自己来完成,最终受益的也是程序员自己。可以这么说,程序员有责任编写功能代码,同时也就有责任为自己的代码编写单元测试。执行单元测试,就是为了证明这段代码的行为和我们期望的一致。
单元测试的内容包括- 模块接口测试
- 局部数据结构测试
- 路径测试
- 错误处理测试
- 边界测试
1)模块接口测试
模块接口测试是单元测试的基础。只有在数据能正确流入、流出模块的前提下,其他测试才有意义。模块接口测试也是集成测试的重点,这里进行的测试主要是为后面打好基础。测试接口正确与否应该考虑下列因素
- 输入的实际参数与形式参数的个数是否相同
- 输入的实际参数与形式参数的属性是否匹配
- 输入的实际参数与形式参数的量纲是否一致
- 调用其他模块时所给实际参数的个数是否与被调模块的形参个数相同;
- 调用其他模块时所给实际参数的属性是否与被调模块的形参属性匹配;
- 调用其他模块时所给实际参数的量纲是否与被调模块的形参量纲一致;
- 调用预定义函数时所用参数的个数、属性和次序是否正确;
- 是否存在与当前入口点无关的参数引用;
- 是否修改了只读型参数;
- 对全程变量的定义各模块是否一致;
- 是否把某些约束作为参数传递。
如果模块功能包括外部输入输出,还应该考虑下列因素:
- 文件属性是否正确;
- OPEN/CLOSE语句是否正确;
- 格式说明与输入输出语句是否匹配;
- 缓冲区大小与记录长度是否匹配;
- 文件使用前是否已经打开;
- 是否处理了文件尾;
- 是否处理了输入/输出错误;
- 输出信息中是否有文字性错误。
- 局部数据结构测试;
- 边界条件测试;
- 模块中所有独立执行通路测试;
(2)局部数据结构测试
检查局部数据结构是为了保证临时存储在模块内的数据在程序执行过程中完整、正确,局部功能是整个功能运行的基础。重点是一些函数是否正确执行,内部是否运行正确。局部数据结构往往是错误的根源,应仔细设计测试用例,力求发现下面几类错误:
- 不合适或不相容的类型说明;
- 变量无初值;
- 变量初始化或省缺值有错;
- 不正确的变量名(拼错或不正确地截断);
- 出现上溢、下溢和地址异常。
(3)边界条件测试
边界条件测试是单元测试中最重要的一项任务。众所周知,软件经常在边界上失效,采用边界值分析技术,针对边界值及其左、右设计测试用例,很有可能发现新的错误。边界条件测试是一项基础测试,也是后面系统测试中的功能测试的重点,边界测试执行的较好,可以大大提高程序健壮性。
(4)独立路径测试
在模块中应对每一条独立执行路径进行测试,单元测试的基本任务是保证模块中每条语句至少执行一次。测试目的主要是为了发现因错误计算、不正确的比较和不适当的控制流造成的错误。具体做法就是程序员逐条调试语句。常见的错误包括:
- 误解或用错了算符优先级;
- 混合类型运算;
- 变量初值错;
- 精度不够;
- 表达式符号错。
(5)错误处理测试
检查模块的错误处理功能是否包含有错误或缺陷。例如,是否拒绝不合理的输入;出错的描述是否难以理解、是否对错误定位有误、是否出错原因报告有误、是否对错误条件的处理不正确;在对错误处理之前错误条件是否已经引起系统的干预等。
通常单元测试在编码阶段进行。在源程序代码编制完成,经过评审和验证,确认没有语法错误之后,就开始进行单元测试的测试用例设计。利用设计文档,设计可以验证程序功能、找出程序错误的多个测试用例。对于每一组输入,应有预期的正确结果。
测试框架
xUnit是各种代码驱动测试框架的统称,这些框架可以测试 软件的不同内容(单元),比如函数和类。xUnit框架的主要优点是,它提供了一个自动化测试的解决方案。可以避免多次编写重复的测试代码。
底层是xUnit的framwork,xUnit的类库,提供了对外的功能方法、工具类、api等
TestCase(具体的测试用例)去使用framwork
TestCase执行后会有TestResult
使用TestSuite控制TestCase的组合
TestRunner执行器,负责执行case
TestListener过程监听,监听case成功失败以及数据结果,输出到结果报告中
面向特定语言的,基于xUnit框架的自动化测试框架
Junit : 主要测试用Java语言编写的代码
CPPunit:主要测试用C++语言编写的代码
unittest , PyUnit:主要测试用python语言编写的代码
MiniUnit: 主要用于测试C语言编写的代码
三、实验过程
3.1源码如下:
package test.lifeGame; import java.awt.Graphics; import javax.swing.JPanel; import java.util.Random; /** * Active 表示活,Dead 表示死 */ public class gameDemo extends JPanel implements Runnable { static enum CellStatus { Active , dead } private boolean isCancel = false; //自定义一个终止标志位 private CellStatus [][] generation1 ; private CellStatus [][] generation2 ; private int MaxRow = 50 ; private int MaxCol = 50 ; private int rows; private int cols; private float frequenceRate = 1 ; @Override public void run() { System.out.println("线程开始") ; while (true) { synchronized (this) { while ( isCancel ) { try { this.wait() ; } catch (InterruptedException e) { e.printStackTrace() ; } } repaint() ; try { sleep() ; } catch (InterruptedException e) { e.printStackTrace() ; } changeGeneration() ; } } } public gameDemo(int rows , int cols) { generationRandom(rows , cols ) ; } /** * 根据给出得行列创建世界,初始化随机生成矩阵 * @param rows * @param cols */ public void generationRandom(int rows , int cols ) { synchronized(this) { if ( ! (rows <= MaxRow && cols <= MaxCol) ) { System.out.println("cols,rows error") ; return ; } this.rows = rows ; this.cols = cols ; generation1 = new CellStatus[cols][rows] ; generation2 = new CellStatus[cols][rows] ; for ( int i = 0 ; i < this.cols ; i++) for (int j = 0 ; j < this.rows ; j++) { Random r = new Random() ; int z = r.nextInt(100); if ( z > 50 ) { generation1[i][j] = CellStatus.Active; } else { generation1[i][j] = CellStatus.dead; } } this.notifyAll(); } } /** * 根据规则改变矩阵 * @return */ public int changeGeneration() { for(int i=0; i < rows; i++) { for(int j=0; j <cols ; j++) { judgeCellStatus(i,j); } } CellStatus[][] temp =null; temp = generation1; generation1 = generation2; generation2 = temp; for(int i=0 ; i < rows ; i++) { for(int j=0 ; j < cols ; j++) { generation2[i][j] = CellStatus.dead; } } return 1; } /** * 判断每个细胞周围的活细胞个数 并且改变下一代的这个细胞的状态 * @param col * @param row * @return */ int judgeCellStatus(int col , int row) { int activeCount = 0 ; if( (col-1) >= 0 && (row-1) >= 0 && (generation1[col-1][row-1] == CellStatus.Active) ) activeCount++;// if( (col-1) >= 0 && (generation1[col-1][row] == CellStatus.Active)) activeCount++; if( (col-1) >= 0 && (row+1) < rows && (generation1[col-1][row+1] == CellStatus.Active)) activeCount++; if( (row-1) >= 0 && (generation1[col][row-1] == CellStatus.Active)) activeCount++;// if( (row+1) < rows && (generation1[col][row+1] == CellStatus.Active )) activeCount++; if( (col+1) < cols && (row-1) >= 0 && (generation1[col+1][row-1] == CellStatus.Active)) activeCount++; if( (col+1) < cols && (generation1[col+1][row] == CellStatus.Active)) activeCount++; if( (col+1) < cols && (row+1) < rows && (generation1[col+1][row+1] == CellStatus.Active)) activeCount++; if(activeCount == 3) { generation2[col][row] = CellStatus.Active; } else if(activeCount == 2) { generation2[col][row] = generation1[col][row]; } else { generation2[col][row] = CellStatus.dead; } return activeCount; } @Override protected void paintComponent(Graphics g) { super.paintComponent(g) ; for (int i=0 ; i < rows; i++ ) { for (int j = 0; j < cols; j++) { if (generation1[i][j] == CellStatus.Active) { g.fillRect(j * 10, i * 10, 10, 10); } else { g.drawRect(j * 10, i * 10, 10, 10); } } } } /** * 终止开启线程刷新 */ public void cancle() { isCancel = true; } public void Start() { isCancel = false; } public void sleep() throws InterruptedException { int rate = (int) frequenceRate * 1000; Thread.sleep(rate); } public float getFrequenceRate() { return frequenceRate; } /** * 修改刷新得频率,默认1s * @param frequenceRate */ public void setFrequenceRate(float frequenceRate) { if(frequenceRate > 0 && frequenceRate < 10){ this.frequenceRate = frequenceRate; } else { return ; } } public CellStatus[][] getGeneration1() { return generation1; } public CellStatus[][] getGeneration2() { return generation2; } }
3.2
? 测试用例设计
模块接口测试:程序中含有三个数据输入,分别是行、列、刷新率,这三个输入都能在程序运行中直观感受到。如果输入的值不能狗准确处理那么界面一定会产生相应的异常。
边界用例:输入为程序所设定的边界,进行测试
错误用例:将输入值设定为错误的范围或者错误的类型
用例设计如下
1.生成矩阵测试用例
用例描述 输入参数 预测结果 正常用例 rows = 10,cols=10 矩阵大小为10*10 错误用例 rows = 51,cols =51 报错 边界用例 rows = 50,cols=50 矩阵大小为50*50 2.矩阵状态刷新用例
用例描述 输入参数 预测结果 正常用例 无 随着时间的变化,此时细胞图不断变化 3.改变矩阵大小测试用例
用例描述 输入参数 预测结果 正常用例 原来的参数:10 10 新的参数:50 50 矩阵大小改变 3.3测试框架介绍与安装过程
? 1.因为本程序是基于java语言编写的,所以采用Junit。
? 2.安装过程
? 在Market中搜索Junit,并安装。
? 并进行如下配置
?
右击工作区选择go to ,新建测试,并选择需要测试的方法。
![](https://img2020.cnblogs.com/blog/1941584/202006/1941584-20200603184712713-1737820926.png) ![](https://img2020.cnblogs.com/blog/1941584/202006/1941584-20200603184703746-1832767368.png)
3.4测试代码
? 生成矩阵测试用例
package test.lifeGame; import org.junit.Assert; import org.junit.Test; /** * 测试随机生成函数 */ public class TestRandom { private gameDemo game = new gameDemo(50,50); @Test public void generationRandom() { game.generationRandom(20,30); Assert.assertEquals(game.getGeneration1()[0].length,20); Assert.assertEquals(game.getGeneration1().length,30); } @Test public void generationRandom2(){ game.generationRandom(50,50); Assert.assertEquals(game.getGeneration1()[0].length,50); Assert.assertEquals(game.getGeneration1().length,50); } @Test(expected = NegativeArraySizeException.class) public void generationRandom3(){ game.generationRandom(-1,-1); } @Test public void generationRandomOut(){ game.generationRandom(51,51); } }
矩阵大小和矩阵刷新测试用例代码
package test.lifeGame; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import static org.junit.Assert.*; /** * 测试按照规则更新矩阵 以及判断方格周围格子状态 */ public class TestChangeAndJudge { private gameDemo game = new gameDemo(10,10); @Before public void setUp() throws Exception { System.out.println("测试开始"); } @After public void tearDown() throws Exception { System.out.println("测试结束"); } @Test public void changeGeneration() { gameDemo.CellStatus[][] statuses = game.getGeneration1(); showCellStatus(statuses); game.changeGeneration(); showCellStatus(game.getGeneration1()); } @Test public void judgeCellStatus() { showCellStatus(game.getGeneration1()); int count = game.judgeCellStatus(0,0); System.out.println("count:"+count); } @Test(expected = ArrayIndexOutOfBoundsException.class) public void judgeCellStatusOut() { int count = game.judgeCellStatus(10,10); System.out.println("count:"+count); } @Test public void judgeCellStatusNormal() { game.judgeCellStatus(5,5); } public void showCellStatus(gameDemo.CellStatus[][] statuses){ for(int i=0;i<10;i++){ for(int j=0;j<10;j++){ System.out.printf("%8s",statuses[i][j]); } System.out.println(); } System.out.println("***********完成***************"); } } package test.lifeGame; import org.junit.After; import org.junit.Test; import org.mockito.Mock; import static org.junit.Assert.*; public class TestThreadRun { private gameDemo game = new gameDemo(10,10); Thread thread; @After public void tearDown() throws Exception{ thread.stop(); } /** * 测试根据规则改变矩阵线程的函数 */ @Test public void run() { thread = new Thread(game); thread.start(); } }
3.5测试结果
? 随机生成矩阵,动态刷新,动态改变大小都都通过测试。如下图所
?
![](https://img2020.cnblogs.com/blog/1941584/202006/1941584-20200603184637443-299620593.png) ![](https://img2020.cnblogs.com/blog/1941584/202006/1941584-20200603184628209-684993499.png) ![](https://img2020.cnblogs.com/blog/1941584/202006/1941584-20200603184612177-1748812674.png) ![](https://img2020.cnblogs.com/blog/1941584/202006/1941584-20200603184600150-1013853130.png)
当输入cols为50abc时,程序报错,错误如下:
?
如图显示,当输入的数据不是int时,则会产生参数不合法的错误。程序挺住运行。
3.6 提交检查结果导仓库
? 已完成
四、思考题与小结
小结:
? 在本次实验中完成了单元测试的工作,编写了一些程序来查找出原来程序中可能存在的问题,主要的问题程序都能检测识别出来,但是仍然有一些问题无法检测,比如说参数类型不匹配。除此之外,这次实验还让我体会到程序的模块化十分重要,模块好的程序不仅仅利于编写理解,更有利于后期的修改测试。方便了编写程序的人,也方便了看程序的人。当完成比较大的项目是,模块化设计则更显重要。
思考题:
比较以下二个工匠的做法,你认为哪种好?结合编码和单元测试,谈谈你的认识。
![](https://img2020.cnblogs.com/blog/1941584/202006/1941584-20200603184813586-754487706.png)
? 答:我认为工匠二刚好一点。工匠一的特点在于要求每一个砖块都是完美的。这种方法
就像在编程过程中立刻解决眼前遇到的问题,虽然眼前的问题是解决了,但是会浪费很多的时间,同时有可能产生出新的问题。如果采用工匠二的方法来进行编程,则开发速度会加快,且可以保证这个模块是可以完成相应的任务,同时如果产生错误,这要在这个模块里进行修改即可。