zoukankan      html  css  js  c++  java
  • 单元测试之初识

    什么是单元测试?

    1、定义

    单元测试(模块测试)是开发者编写的一小段代码,用于检验被测代码的一个很小的、很明确的功能是否正确。通常而言,一个单元测试是用于判断某个特定条件(或者场景)下某个特定函数的行为。

    下面我们来看一个小例子:

    判断是否为三角形的函数:

    测试用例:

     

    2、规则

    1)覆盖程序的每个分支。对于if语句,需要测试每个if以及else场景,对于循环语句,如for(i=3;i<k;i++),需要设计k>3,k=3两个场景。

    2)结果输出必须要严格定义。如果我们当前测试的函数具有返回值,那么必须要对返回结果进行严格定义。

    例如 return strBnkName; 其中strBnkNameString型,则必须定义好在无数据时strBnkName为空字符串还是null。其它数据类型或者自定义数据类型亦应如此。

    3)要检测的数据必须可以断言。

    一般开发代码写完后都会做自测,但是大多数都只走正常场景,而且是一个功能的自测,而不是一个函数或者是一个小模块。

     

    为什么要做单元测试?

    1尽早消灭缺陷。开发人员有义务提供一个行为正确的class,也有权利获得一个行为正确的class。当合作伙伴调用你的每个class都是行为正确的,当你调用别人的class也总没有报错,将会使工作顺利愉快很多。

    2、减少后续排查bug的工作量。如果没有经过单元测试,当一个产品开发完成后,一调试,发现有很多的bug,需要去不断的断点调试,会是一件非常痛苦的事。说不定这个产品不可用仅仅是因为一个函数输出错误,你会失眠的,哈哈。其实很简单的一个道理,在硬件系统中,谁敢将没有经过测试的零件进行组装呢?

    3、增加项目可控程度。做了单元测试之后,通过的功能一般就没有问题了,可以很好的控制项目进度。如果没有单元测试,可能到项目后期,发现开发好的功能居然是不能用的,那么就会使项目有延期风险。

    4、提高系统可信赖度。相信每个函数都经过测试的代码,大家都会信赖的,自己也会非常有信心,对客户更是一种负责。

    5、便于后续人员的接手和管理。试想当你来到一家新公司,接受了别人写的代码,然后要在上面修改功能或增加功能,写着写着有问题了,这个时候若有成套的单元测试,你就可以直接来找问题,而不必痛苦的一行行阅读别人的代码断点调试了。既然如此,我们也应该为我们自己的代码负责,给后面的码农朋友提供便利。

     

    为什么开发不愿意做单元测试?

    1、单元测试属于测试工作。在大多数开发人员眼里,“开发”和“测试”是两个泾渭分明的范畴,他们认为:开发人员的工作就是写新代码,实现新功能,至于代码的测试,那是测试人员的职责,我只要让代码编译通过就行了。软件很复杂很抽象,我们不能强求开发人员开发的软件没有缺陷,因此需要测试人员把关,但是至少要保证写的每个方法能产出预期的结果,那么单元测试正是开发人员所必要的。

    2、单元测试受益的是测试而不是开发。其实单元测试受益最大的人莫过于开发了。有了单元测试,你可以随时从同事手中接过值得信赖的代码;有了单元测试,你可以随时保证你写的代码行为正确;有了单元测试,你可以随时通过自动化操作得知某个Class行为是否正确;有了单元测试,你以后的Debug和重构工作将变得轻松异常。

    3、集成测试会抓住所有bug。很多开发会认为所有bug都会在集成测试的时候找出来。而事实上,规模越大的代码集成意味着复杂性就越高。如果软件的单元没有事先进行测试,开发人员很可能会花费大量的时间仅仅是为了使软件能够运行,而任何实际的测试方案都无法执行。另一方面,即便软件运行成功了,考虑软件全局复杂性的前提下对每个单元进行全面的测试。这是一件非常困难的事情,甚至在创造一种单元调用的测试条件的时候,要全面的考虑单元的被调用时的各种入口参数。在软件集成阶段,对单元功能全面测试的复杂程度远远的超过独立进行的单元测试过程。 这种情况下,结果就是测试将无法达到它所应该有的全面性。一些缺陷将被遗漏,并且很多Bug将被忽略过去。

    4、投入产出比低。一旦编码完成,开发人员总是会迫切希望进行软件的集成工作,这样他们就能够看到实际的系统开始启动工作了。而做单元测试则被认为是一种浪费大量时间的行为,在做的过程中没有体会到实质性的收获。但是这种情况下软件集成了也可能无法工作,即便能投入使用也无法确保它能够可靠运行。有认为投入产出比的思想的开发大多也是应为没有尝试过贯彻单元测试,没有真正体会到单元测试带来的受益。

    5、最后一点就是从人性的角度考虑了。一般人都会只顾眼前利益,比如大家都知道每天锻炼对身体好,每天多吃蔬果对身体好,又有几个人坚持呢?何况很多人都认为我开发完了这个功能,说不定后面我就干别的去了,做那么多工作自己也受不了益,但是确没想到要是别人也交给自己一堆没有经过很好测试的代码会如何头疼。就好比生产三聚氰胺牛奶的人,他会想我只要不和牛奶就行了,却没想到他会吃到别人制作的地沟油。

     

    怎么做单元测试?

    1、黑盒测试

    黑盒测试时根据程序的功能点进行测试设计。如果黑盒测试是足够充分的,那么白盒测试就没有必要,可惜“足够充分”只是一种理想状态,程序的功能点是人为的定义,常常是不全面的;各个输入数据之间,有些组合可能会产生问题,怎样保证这些组合都经过了测试?难于衡量测试的完整性是黑盒测试的主要缺陷,而白盒测试恰恰具有易于衡量测试完整性的优点,两者之间具有极好的互补性,例如:完成功能测试后统计语句覆盖率,如果语句覆盖未完成,很可能是未覆盖的语句所对应的功能点未测试。

    2、白盒测试

    白盒测试时针对程序的逻辑结构来设计测试用例,用逻辑覆盖率来衡量测试的完整性。逻辑单位主要有:语句、分支、条件、条件值、条件值组合,路径。条件覆盖是指覆盖所有的条件表达式,即所有的条件表达式都至少计算一次,不考虑计算结果;条件值覆盖是指覆盖条件的所有可能取值,即每个条件的取真值和取假值都要至少计算一次;条件值组合覆盖是指覆盖所有条件取值的所有可能组合。

    讲到这里,做过单元测试的同学可能会提出质疑了,要做所有逻辑单位的覆盖几乎是不可能的,即便做到付出的成本也会非常高。

    其实我们可以借助一些工具,可以在较低的成本下达到这种测试要求,后面将会作进一步介绍。

    3、黑盒测试+白盒测试

    黑盒测试不能保证测试覆盖率,白盒测试又会花费较大的成本。那么我们何不利用两者的优点进行互补呢?

    我们先进行黑盒测试,然后统计白盒覆盖率,针对未覆盖的逻辑单位设计测试用例覆盖它,例如,先检查是否有语句未覆盖,有的话设计测试用例覆盖它,然后用同样方法完成条件覆盖、分支覆盖和路径覆盖,这样的话,既检验了黑盒测试的完整性,又避免了重复的工作,用较少的时间成本达到非常高的测试完整性。

     

    单元测试工具

    单元测试的量非常庞大,如果要开发一个个去写,显然会比较郁闷。为了解决这个问题,牛人就发明了xxUnit,针对不同语言设计的。下面我们简单介绍下针对Java的单元测试工具:

    1JUnitJUnit 是 Java 社区中知名度最高的单元测试工具。JUnit 设计的非常小巧,但是功能却非常强大。JUnit ——是一个开发源代码的Java测试框架,用于编写和运行可重复的测试。他是用于单元测试框架体系xUnit的一个实例(用于java语言)。主要用于白盒测试,回归测试。

    2JUnit-addons。对JUnit的一些补充,比如设置、获取被测试对象的私有属性的值,调用被测试对象的私有方法等。

    3Spring测试框架。可以测试基于Spring的应用,通过配置文件和注解自动组装需要的单元测试对象。

      提供了一些常用的J2EE Mock对象,比如HttpSessionMock类等。

    4DJUnit。通过代码自动产生Mock对象,省去了自己手动编写N多的Mock类。

    此外,它的Eclipse插件还可以做到测试覆盖率、分支统计。

    5、EasyMock。功能同DJUnit,也是通过编程自动Mock掉与测试对象无关的类,方法。

     

    JUnit实践

    JUnit是使用最广泛的java单元测试工具。以JUnit4为例,我们来看看怎么用这个工具来做单元测试:

    一、自动生成用例

    下面是一个数据计算工具函数,里面有加减乘除,乘方,开方,以及清零函数。里面的程序故意留了几个bug,供我们测试用

    package UnitTestExample;

    public class MathTool {
        private int result; // 静态变量,用于存储运行结果
        /**
         * 加法函数
         * @param n
         */
        public void add(int n){
            result = result + n;
        }
        /**
         * 减法函数
         * @param n
         */
        public void substract(int n){
            result = result -1; //Bug: 正确的应该是 result =result-n
        }
        /**
         * 乘法函数
         * @param n
         */
        public void multiply(int n){
        } // 此方法尚未写好
        /**
         * 除法函数
         * @param n
         */
        public void divide(int n){
            result = result / n;
        }
        /**
         * 乘方函数
         * @param n
         */
        public void square(int n){
            result = n * n;
        }
        /**
         * 开方函数
         * @param n
         */
        public void squareRoot(int n){
            for (; ;);//Bug : 死循环
        }
        /**
         * 清零函数
         */
        public void clear(){ // 将结果清零
            result = 0;
        }
        /**
         * 获取结果
         * @return
         */
        public int getResult(){
            return result;
        }
    }

    在JUnit中为这个class自动生成单元测试可以按以下步骤进行:

    第一步,从网上下载junit.jar,存放在本地。

    第二步,打开eclipse,新建一个项目取名为UnitTestExample,右键点击项目名称,选择properties

     

    接下来选择java路径,将刚才下的junit.jar包引进来

    第三步,新建一个类,取名为MathTool,将数据计算工具的程序黏贴进去

    第四步,为这个class生成单元测试用例,单击要测试的类名右键点击,选择新建JUnit Test Case:

     

    然后设置好选项

     

    点击Next,选择要测试的方法,比如我们只测试加减乘除

     

    点击Finish后,单侧用例就自动生成了,如下图

    自动生成的单侧用例是空的,我们可以按需求写断言进行测试。

    二、执行测试

    我们对空的用例进行修改,得到我们想要的用例,如下:

    package UnitTestExample;

    import static org.junit.Assert.*;
    import org.junit.Before;
    import org.junit.Ignore;
    import org.junit.Test;

    public class MathToolTest1 {
        private static MathTool MathTool = new MathTool();//如果这里不新建一个MathTool的对象,调用它里面的方法就调用不到
                                                          //除非将里面的方法前都加上static
        @Before
        public void setUp() throws Exception {
            MathTool.clear();
        }

        @Test
        public void testAdd() {
            MathTool.add(2);
            MathTool.add(3);
            assertEquals(5,MathTool.getResult());
        }

        @Test
        public void testSubstract() {
            MathTool.add(10);
            MathTool.substract(2);
            assertEquals(8,MathTool.getResult());
        }
        /**
         * 忽略用例
         */
        @Ignore
        @Test
        public void testMultiply() {
            MathTool.add(10);
            MathTool.multiply(2);
            assertEquals(20,MathTool.getResult());
        }

        @Test
        public void testDivide() {
            MathTool.add(8);
            MathTool.divide(2);
            assertEquals(4, MathTool.getResult());
        }
    }

    执行测试可以得到如下结果:

     三、标注说明

     我们看到在用例前我们都会加标注,下面我们来介绍下各个标注的用途:

    @Before

    我们的变量是一个全局变量,在执行完一个用例后,它的值就改变了,若不将它恢复,就会影响下一个用例执行。为了保证用例间的独立性,我们会在运行完每个用例后对变量或操作或对象的状态进行复原,@Before就是表明这个方法在每个用例执行之前都执行一遍。

    @Test

    标明是一个测试用例,可以按JUnit进行运行。

    @Ignore

    标明忽略这个用例,不执行。

    除此之外,还有:

    @After

    标明在每个用例执行完的时候都执行一遍该方法。

    @BeforeClass @AfterClass

    有一些方法,比如是初始化某人群体的状态,初始化后每个用例都能用并且不冲突;或者读取一个大文件,需要耗费较长的时间。显然这种情况下还用@Before,@After是不合理的。那么有其他方法解决吗?答案是肯定的。@BeforeClass和@AfterClass就是这种情况下使用的,在类初始化之前,之后运行。

    四、限时测试

    当某个测试用例进入死循环后,相信你会非常郁闷。而那些逻辑很复杂,循环嵌套比较深的程序,很有可能出现死循环,因此一定要采取一些预防措施。限时测试是一个很好的解决方案,如下:

    五、测试异常

    要测试的函数肯定有一些是需要抛出异常的,如果没有抛出异常,就说明程序有问题了。我们知道try catch可以捕捉异常,那有没有方法可以测试有没有抛出异常呢?下面就是测试异常的方法:

     

    六、Runner

    可能很多人都会疑惑,JUnit是怎么运行代码的呢?答案就是Runner。在JUnit中有很多个 Runner,他们负责调用你的测试代码,每一个Runner都有各自的特殊功能,你要根据需要选择不同的Runner来运行你的测试代码。可能你会觉得 奇怪,前面我们写了那么多测试,并没有明确指定一个Runner啊?这是因为JUnit中有一个默认Runner,如果你没有指定,那么系统自动使用默认 Runner来运行你的代码。

    七、参数化测试

    一开始讲什么是单元测试的时候,我们就讲到了单元测试要覆盖程序的每个分支。对于MathTool 这个class中的乘方函数square,我们要设计3种情况,正数,负数,0。设计测试用例如下:

     

    可能大家看到这个函数并不觉得特别繁琐,但是如果程序的分支比较多,或者需要考虑的入参情况比较多样,那用例设计起来就非常繁琐了,这个时候,我们就需要参数化测试。把这若干种情况作为参数传递进去,一次性的完成测试。将上面的用例用参数化方法设计可以设计成如下:

     

    执行用例后,运行结果如下:

    测试人员在单元测试中的定位

    开发人员不愿意做单元测试还有一个借口,那就是我们都做了单元测试了,还要测试做什么?测试人员也会担心,单元测试搞起来了,我们是不是就要失业了呢?至少功能测试人员要大缩减了。其实我们可以和开发同学一起搞单元测试的。一个完整的单元测试,包含以下几部分:

    1、设计框架

    2、设计用例

    3、编码

    4、统计覆盖率

    开发不是就喜欢编码吗?那我们就可以从另外三个方面侵入。据我搞过单元测试的同事说,他在单元测试中就承担了设计框架、设计用例、统计覆盖率工作,后续还可能参与编码工作。这样不仅贯彻了测试驱动开发,更实现了测试人员驱动开发人员。这样我们不但不会失业,还会占据主导权。

    当然这需要深厚的技术基础以及丰厚的工作经验。事实上,软件发展的方向就是如此,如果有一天,成品软件没有bug了,我们也壮烈牺牲了,那么请把我们埋在曾经的辉煌里~



  • 相关阅读:
    [HDU] 1269 迷宫城堡最简单的强连通分支题
    [HDU] 1301 Jungle Roads赤裸裸的最小生成树
    [HDU] 2647 Reward带有贪心算法的拓扑排序
    [HDU] 1181 变形课简单建模后广搜
    【慢慢学Android】:5.短信拦截
    【慢慢学Android】:8.获取所有短信
    ubuntu下设置Android手机驱动
    Win7笔记本变为热点供手机上WiFi
    VS快捷键和小功能
    【慢慢学Android】:11.对话框大全
  • 原文地址:https://www.cnblogs.com/taobaomercury/p/2370290.html
Copyright © 2011-2022 走看看