实验五、单元测试

实验五、单元测试

一、实验目的

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语言编写的代码

三、实验过程

3.1 程序源码

package lifegame;

public class Methods {
     public Methods() {
        }

        //该方法检测所有位置,并返回对应位置的point数组
        //用point数组,记录对应位置下一轮的状态,1下一代死,2下一代继续活,3下一代复活
        public int[] check(String[][] lifeMap, int[] point) {

            int n = 0;
            //统计周围邻居的情况
            for (int i = 0; i < 4; i++) {
                for (int j = 0; j < 4; j++) {

                    /*
                    规则如下,进行判断
                                      ( 1)一个人可以有8个邻居;
                                      ( 2)一个人若只有一个邻居,在下一代会孤独的死去;
                            (3)若有2或3个邻居,在下一代依然活着;
                            (4)若有4个或以上邻居,在下一代会因拥挤而死;
                            (5)死去的人若有3个邻居,在下一代会复活;
                            (6)所有的死去或复活都在下一代变化时同时发生。
                    */

                    //用life变量记录周围活着的邻居个数
                    int life = 0;

                    //1.判断正下方的位置
                    if (i + 1 < 4 && lifeMap[i + 1][j].equals("●")) {
                        life++;
                    }

                    //2.判断右下位置
                    if (i + 1 < 4 && j + 1 < 4 && lifeMap[i + 1][j + 1].equals("●")) {
                        life++;
                    }

                    //3.判断左下位置
                    if (i + 1 < 4 && j - 1 >= 0 && lifeMap[i + 1][j - 1].equals("●")) {
                        life++;
                    }

                    //4.判断右侧位置
                    if (j + 1 < 4 && lifeMap[i][j + 1].equals("●")) {
                        life++;
                    }

                    //5.判断左侧位置
                    if (j - 1 >= 0 && lifeMap[i][j - 1].equals("●")) {
                        life++;
                    }

                    //6.判断正上方位置
                    if (i - 1 >= 0 && lifeMap[i - 1][j].equals("●")) {
                        life++;
                    }

                    //7.判断右上位置
                    if (i - 1 >= 0 && j + 1 < 4 && lifeMap[i - 1][j + 1].equals("●")) {
                        life++;
                    }

                    //8.判断左上位置
                    if (i - 1 >= 0 && j - 1 >= 0 && lifeMap[i - 1][j - 1].equals("●")) {
                        life++;
                    }

                    //用一个数组,记录对应位置下一轮的状态,1下一代死,2下一代继续活,3下一代复活
                    if (lifeMap[i][j].equals("●")) {
                        if (life == 1)
                            point[n] = 1;
                        else if (life == 2 || life == 3)
                            point[n] = 2;
                        else if (life >= 4)
                            point[n] = 1;
                    } else {
                        if (life == 3)
                            point[n] = 3;
                    }
                    n++;
                }
            }
            return point;
        }

        public String[][] getNext(String[][] lifeMap, int[] point) {

            int n = 0;
            for (int i = 0; i < 4; i++) {
                for (int j = 0; j < 4; j++) {

                    //变更状态
                    if (point[n] == 1)
                        lifeMap[i][j] = "○";
                    if (point[n] == 3)
                        lifeMap[i][j] = "●";

                    n++;
                }
            }
            return lifeMap;
        }

        public void printLifeMap(String[][] lifeMap){
            for (int i = 0; i < 4; i++) {
                for (int j = 0; j < 4; j++) {
                    if (j == 3)
                        System.out.println(lifeMap[i][j] + " ");
                    else
                        System.out.print(lifeMap[i][j] + " ");
                }
            }
        }
}

Methods.java

package lifegame;

import java.util.Random;
import java.util.Scanner;

    /*
        (1)生命小游戏的邻居为上下左右和斜对角一共八个位置
        (2)默认选择4*4的格子
        (3)将默认的格子初始化,并打印输出
        (4)使用Methods中的方法生成下一轮的状态,并打印
     */

public class LifeGame {
    public static void main(String[] args) {

            //设置一个二维数组存储所有的格子
            String[][] lifeMap = new String[4][4];

            Methods me = new Methods();

            //将所有格子进行初始化输入,死为0,活为1
            //随机生成各个位置的邻居情况
            for (int i = 0; i < 4; i++) {
                for (int j = 0; j < 4; j++) {

                    int num = new Random().nextInt(2);
                    if (num == 1)
                        lifeMap[i][j] = "●";
                    else if (num == 0)
                        lifeMap[i][j] = "○";
                }
            }

            //打印格子初始状态
            System.out.println("初始状态为:");
            me.printLifeMap(lifeMap);
            System.out.println("===========");
            
            Scanner scan = new Scanner(System.in);
            int n = 0;
            int num = 0;//记录变化的次数
            while (n == 0) {

                //用point数组,记录对应位置下一轮的状态,1下一代死,2下一代继续活,3下一代复活
                int[] point = me.check(lifeMap, new int[16]);

                //将获得下一次变化后的图形
                lifeMap = me.getNext(lifeMap, point).clone();

                System.out.println("第" + (++num) + "次变化:");

                //打印出来
                me.printLifeMap(lifeMap);
                System.out.println("===========");

                System.out.println("输入0继续进行下一步,输入其他数字退出。");
                if(scan.hasNextInt()) {
                    n = scan.nextInt();
                }
                else n = 1;
            }
            scan.close();
        }
}

LifeGame.java

3.2 测试用例设计

用4*4的矩阵代表状态,"●"表示活,"○"表示死

用point数组,记录对应位置下一轮的状态,0:上下代为死 1:下一代死,2:下一代继续活,3:下一代复活

check方法测试用例

用例参数期望结果

{"●","●","○","○"},

{"○","○","○","○"},

{"●","○","●","○"},

{"○","●","●","○"}

{1,1,0,0,3,0,0,0,1,0,2,0,0,2,2,0}

getNext方法测试用例

用例参数期望结果

{"●","●","○","○"},

{"○","○","○","○"},

{"●","○","●","○"},

{"○","●","●","○"}

{1,1,0,0,3,0,0,0,1,0,2,0,0,2,2,0}

{"○","○","○","○"}

{"●","○","○","○"}

{"○","○","●","○"}

{"○","●","●","○"}

 

 

 

 

 

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

JUnit测试框架介绍

因为此次实验代码是由Java语言编写而成,从而采用JUnit。JUnit是一个开发源代码的Java测试框架,用于编写和运行可重复的测试。它是用于单元测试框架体系xUnit的一个实例(用于java语言)。Junit有其自己的JUnit扩展生态圈,多数Java的开发环境已集成了JUnit作为单元测试的工具,用于测试期望结果的断言,用于共享共同测试数据的测试工具,用于方便的组织和运行测试的测试套件及图形和文本的测试运行器。Eclipse 集成了 JUnit,可以非常方便地编写 Test Case。Eclipse 自带了一个 JUnit 插件,不用安装就可以在项目中测试相关的类,并且可以调试测试用例和被测类。

安装过程

打开eclipse,进入项目文件的属性

 实验五、单元测试

   选择Java构建路径中的库(L),点击ADD Library

实验五、单元测试

   添加库类型,选择Junit

实验五、单元测试

   在Junit库版本中选择Junit4

实验五、单元测试

 完成后点击Apply and Close即可

实验五、单元测试

 

3.4 测试代码

选中Methods.java,右击新建—>其他

实验五、单元测试

 选中创建JUnit测试用例,点击下一步

 实验五、单元测试

勾选setUp()(U),点击下一步

实验五、单元测试

选择check和getNext方法

实验五、单元测试

生成初始测试代码

实验五、单元测试

修改代码,改为预期的测试代码

实验五、单元测试

MethodsTest.java

3.5 测试结果与分析

   首次测试,出现错误,即l类型Methods中的方法check对参数不适用

实验五、单元测试

   修改check方法中的代码

实验五、单元测试

将其修改为

实验五、单元测试

将check方法中的参数修改,将参数改到方法内中

 实验五、单元测试

 实验五、单元测试

 再次运行测试,运行成功,测试完成

实验五、单元测试

 

3.6 push测试报告和测试代码到各自的github仓库

实验五、单元测试

四、思考题

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

实验五、单元测试

答:工匠一更好,首先工匠一、二都知道砖有对不齐的可能,而测试人员都知道,软件或多或少会有问题,如果能在开发之前预见开发过程中会出什么样的问题,测试将会有质的改变。开发人员根据测试人员所预见的问题开发,防止这些问题的发生。这是测试过程质的改变,测试不再是去找bug,而是引导开发避免问题的发生。测试的流程和作用也发生了改变,测试不是再等墙砌完了,推了再砌,而是和开发一起努力,争许一次把墙砌好,要以最小返工率为目标。所以在编码的开始阶段,就应该规范进行,确定好整体架构和内容,按照标准规则进行,避免测试时出现较大的错误又重新编码,解决不必要的麻烦。

 五、实验总结

   本次实验是单元测试的实验,即使用一些单元测试的办法来查找程序出现的bug,虽然bug不会影响程序的运行,但是这些bug会影响测试的速度,存在着潜在隐患,对测试人员和开发人员产生一定的影响。而通过此次修复bug测试验证实验,学会了Eclipse下junit的使用过程,让我掌握了单元测试的原理和方法,对程序的编码和测试的重要性也进一步地了解。

相关推荐