软件工程实验五---单元测试

一、实验目的

1)掌握单元测试的方法;

  1. 学习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成功失败以及数据结果,输出到结果报告中

1.3面向特定语言的,基于xUnit框架的自动化测试框架

  Junit : 主要测试用Java语言编写的代码

  CPPunit:主要测试用C++语言编写的代码

  unittest , PyUnit:主要测试用python语言编写的代码

  MiniUnit: 主要用于测试C语言编写的代码

三、实验过程

  由于代码运行在Java环境,所以采用Junit来完成测试。

3.1 源码

  Elward-lv的Git源码,未修改的commitId:907a01ac73a4382edca6194dcc4f63045d11c918,代码如下:

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 测试用例设计

  程序主要包含两个类,一个是用于生成GUI的GUI类,和一个用于描述生命游戏世界的gameDemo类。针对于GUI类,由于设计GUI的时候没有进行很好的模块划分,所以只能采用手动的方式进行单元测试,即手动进行数据的输入和事件的触发;对于gameDemo类的随机生成二维数组根据规则改变矩阵判断某个坐标细胞下一次的状态规则刷新线程的运行状态等功能来单独设计测试用例,因为细胞图生成随机所以很难断言具体的参数。

a.随机生成二维数组测试用例    generationRandom(int rows , int cols):输入是矩阵的行列,范围均是0<=x<=50的整数

用例描述 用例参数 期望结果
正常用例 rows = 20,cols = 30 返回20*30的随机字符矩阵
边界用例 rows = 50,cols = 50 返回50*50的随机字符矩阵
错误用例1 rows = 51,cols = 51 矩阵大小不变,超出范围被处理
错误用例2 rows = -1,cols = -1 抛出异常NegativeArraySizeException

b.判断某个坐标细胞下一次的状态用例   judgeCellStatus(int col , int row): 前提是数组的矩阵已经随机生成,输入是矩阵的行列号

用例描述 用例参数 期望结果
正常用例 row=10,col = 10 此细胞周围的活细胞数目
边界用例 row = 0, col = 0 此细胞周围活细胞数目
异常用例 row ,col均大于设定的数组行列 数组越界异常

c.根据规则改变矩阵用例  changeGeneration():前提是数组的矩阵已经随机生成,根据细胞生存规则改变细胞下一次状态,因为没有输入只进行一般测试

用例描述 用例参数 期望结果
一般用例 改变之后的矩阵

d.**规则刷新线程的运行状态用例 **:前提是gameDemo已经初始化,因为没有输入,所以测试只进行一般的测试

用例描述 用例参数 期望结果
一般用例 随着时间的推移,细胞状态图不停刷新

e.针对GUI的测试用例: 主要针对GUI中事件和输入文本,进行手动测试,输入是行列号(和a的输入一样)和变化频率,时间包括一个暂停按钮。点击按钮时会收集输入文本,并依照此来设置新的矩阵参数,所以主要测试GUI的流程的正确性和输入文本的类型。

用例描述 用例参数 期望结果
正常用例 rows = "30",cols = "30",rate = "1.0" 行列改变,按照规则刷新
异常用例 rows = "30cols", cols = "30cols",rate = "0.5" 提示异常的输出信息

3.3 选择的测试框架介绍、安装过程

1)JUnit是由 Erich Gamma 和 Kent Beck 编写的一个回归测试框架(regression testing framework)。Junit测试是程序员测试,即所谓白盒测试,因为程序员知道被测试的软件如何(How)完成功能和完成什么样(What)的功能。Junit是一套框架,继承TestCase类,就可以用Junit进行自动测试了。

2)安装过程

  idea安装Junit:在idea的plugin里面搜索Junit

  然后点击安装,随后配置Junit如下:

  使用方法可以直接鼠标右击选择goto:

  创建新的测试,选择需要测试的方法:

  引入包:由于开始项目是由maven构架,所以只需要改变pom.xml就可以完成Junit包的引入,依赖如下图:

<dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.mockito/mockito-all -->
        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-all</artifactId>
            <version>1.10.19</version>
            <scope>test</scope>
        </dependency>

    </dependencies>

3.4 测试代码

a.随机生成二维数组测试用例

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);
    }
}

b,c,d.判断某个坐标细胞下一次的状态用例和根据规则改变矩阵用例和测试线程的用例

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 测试结果与分析

a.随机生成二维数组测试用例

用例描述 用例参数 实际结果
正常用例 rows = 20,cols = 30 字符矩阵 20* 30 ,通过
边界用例 rows = 50,cols = 50 字符矩阵 50* 50 ,通过
错误用例1 rows = 51,cols = 51 测试通过
错误用例2 rows = -1,cols = -1 测试通过

b,c,d均测试通过,没有问题

e.GUI测试用例

用例描述 用例参数 实际结果
正常用例 rows = "30",cols = "30",rate = "1.0" 没有问题,程序变化成30*30之后,继续运行
错误用例 rows = "30cols", cols = "30cols",rate = "0.5" 出现异常,程序停止,如下图

  经过测试,在含有错误用例的结果可以看出,有一些程序需要处理的异常,分别是数组越界,参数不合法为负,以及GUI读取字符串时含有字母等非法字符导致转化为float时出现问题等。解决方法可以是在程序中捕获这些异常并保持上次的状态,然后提醒用户输入不合法或者其他信息。

3.6 提交检查结果到仓库

四.思考题和小结

小结

  本次实验主要是做了单元测试的工作,使用了一些单元测试的办法来查找程序可能出现的bug,除了少数一些未知的异常除外,我感觉其他的异常在程序的设计阶段都可以通过参数检查和修改来达到控制输入的目的。而且经过这次测试,我明白了模块化设计的作用,不仅仅是方便维护检查,也方便测试,更有利于程序的稳定!以后会努力往此方面靠,尽量做了模块化设计。

思考题

  比较以下二个工匠的做法,你认为哪种好?结合编码和单元测试,谈谈你的认识。

答:其实问题和程序的先检查还是最后检查类似。对于编码来说:编码风格和约定自然是需要在开始的时候就需要确定,这样的话通过注解和数据命名方便对代码进一步的修改,而不需要花费多余的时间去理解可能遗忘了的功能的程序。对于测试来说,工匠一是在写程序的时候就解决可能出现的bug,这样的话自然会消耗更多的开发时间,而且不能保证解决所有的问题,比较适合时间充裕的开发工作;工匠二是先进行编码不去考虑程序中数据超出预期可能出现一些异常,而是留到最后统一测试检查问题,比较适合项目时间紧张的开发。两种方式都有适合的场景,我觉得第二种更好一点,即使是课设时间也都是不是很充足的。

原文地址:https://www.cnblogs.com/elward-lv/p/12954521.html