zoukankan      html  css  js  c++  java
  • Java单元测试技术1

    另外两篇关于介绍easemock的文章:EasyMock 使用方法与原理剖析使用 EasyMock 更轻松地进行测试

    摘要:本文针对当前业软开发现状,先分析了WEB开发的技术特点和单元测试要解决的问题,然后分别阐述了解决这些问题的单元测试技术,内容包括:JUnit、测试桩构建、访问数据库的Java代码测试、Struts框架测试、服务器布署环境下的组件测试、Spring下的单元测试,以及覆盖率检查技术,最后还谈到了测试自动化技术以及希望在业软推广的自动化测试框架和它带来的好处。另外,随本文还附有例子代码供大家参考。

    关键词:Java、Web开发、单元测试、工具、JUnit、EasyMock、DBUnit、Struts、StrutsTestCase、Cactus、Spring、Cobertura、覆盖率检查、自动化测试、例子代码。

    单元测试与开发技术密切相关,业软基于Java的开发一般是WEB应用开发,涉及的开发技术繁多,尤其是现在的开源软件盛行,更给Java增添了无穷的活力和生机,同时也给单元测试增加了复杂度,我们推行单元测试面临着前所未有的挑战,难怪项目组抱怨单元测试难测或测不起来。本文试图给出基于业软开发现状的Java单元测试完整解决方案,以便更加有效地在业软推行单元测试。下面我们从分析开发技术特点以及对单元测试的影响说起。

    1      基于Java开发的技术特点

    u  容器管理的组件开发

    开发WEB应用程序,实质上就是在开发一系列组件。组件的类型有很多,JavaBean、Servlet、Filter、JSP Taglib、EJB、Spring Bean,等等。这些组件一般是不能独立运行的,需要将它们布署到WEB服务器,通过与WEB容器或EJB容器交互才能实现一定的业务逻辑,也就是说,组件依赖的许多对象是运行时由容器创建的,如HttpServletRequest、HttpServletResponse、ServletContext、SessionContext、FilterChain、PageContext,等等,这就面临着单元测试时这些容器对象如何生成的问题。对于这些组件的测试一般我们有两种单元测试方法:一种是对被测组件进行隔离测试,组件依赖的服务器环境对象用桩取代,它的缺点是构建太麻烦;另一种是将组件运行在真实服务器环境下,有别于系统测试,被测对象是我们主动在测试代码中创建的,它的优点是更接近于真实环境、免除了构建的工作量。

    u  页面显示的视图开发

    有很多技术用于将视图与业务逻辑分开,如Struts、JSF、WebWork、Spring等,页面开发的技术也有很多,如Html、XML、JSP、JavaScript、Velocity,等等,对于这些页面文件,技术上很难进行编译静态检查,对它们的测试虽然有一些工具支持,但效果均不理想,而且代码Review大家反映也很难发现实质性的页面问题,如何确保页面文件的质量一直是单元测试要解决的问题。

    u  框架复杂

    WEB开发使用的框架很少有自己独立设计的,一般都会使用现成的架构,如Struts、JSF、WebWork、EJB、Spring、Hibernate、iBATIS等等,不同的框架,能够支持单元测试的程度也是不一样的。

    u  数据库访问技术多

    业软的WEB应用开发很少有不访问数据库的,因为多是面向业务的开发,业务离不开数据存储。项目组可能会选择不同的数据库访问技术,如JDBC、EntityBean、Spring、Hibernate、iBATIS等,对于涉及数据库访问的代码如何做单元测试也是我们需要解决的问题。

    2      单元测试技术需要解决的问题

    有许多的单元测试技术和工具,综合起来,无非是为了解决以下问题。

    u  驱动(Driver)—驱动被测单元

    单元不能独立运行,必须实现调用它们的代码,我们称其为驱动代码,其实最简单的驱动就是实现main方法,大家常用的驱动典型工具就是JUnit。

    u  构建桩(Stub)—模拟被测单元依赖的对象

    被测的孤立单元通常会对其它对象有依赖,这种依赖通常表现在:依赖对象通过被测方法参数传入或者被测对象保存有依赖的对象引用,然后在被测方法中调用了依赖对象的方法。构造依赖对象,一方面我们可以直接将开发完成的并且之前已经过单元测试的代码直接拿过来用,另一方面,也是更常用的方法,就是自己构建模拟对象,我们称其为桩,但自己写桩很麻烦,工作量大,现成构建桩的理想工具是EasyMock。

    u  验证(Verify)

    用例执行是否成功,需要在测试中添加验证点,需要将预期结果与测试执行获得的实际结果进行比较,为此JUnit为我们提供了验证的基本逻辑框架,其它工具可以基于它实现更复杂的验证逻辑,如DBUnit实现的对数据库表数据的验证。

    u  用例管理

    常有同事提到用main方法也能实现对被测单元的驱动,但我觉得最大的不足是无法实现对用例的有效管理,为此JUnit为我们提供了用例管理的基础框架,通过引入测试套的概念将用例有效地组织起来。

    u  结果输出(Report)

    测试结束后要能够将本次运行的结果情况形成报告,并以图形化直观的形式报告给用户。JUnit也为我们做到了,尤其是IDE与JUnit的集成,使我们在开发过程中做单元测试变得更加方便。

    u  覆盖率检查

    公司要求被测代码要求达到语句的100%覆盖,是否覆盖到了,我们可以借助覆盖率检查工具,做测试执行的同时进行覆盖率检查,对未覆盖到的代码可能会发现两类问题:不可达代码,这样的代码需要优化;用例设计不充分,这时就要及时补充用例。常用的覆盖率检查工具有PureCoverage、Cobertura。

    u  测试自动化

    实现测试自动化的好处大家都很明白,方便回归测试,节省了工作量;另一个好处是便于对测试的监控,这一点我们在后面会谈到。我们模索系统测试自动化已有多年,但效果都不理想,主要原因我觉得和系统测试本身的特点有关,因为系统测试是站在用户角度看系统功能的整体表现(这其中最讨厌的是我们经常还有需求变更,如何做到以不变应万变,我们曾经尝试过,但效果均不理想)。但单元测试不同于系统测试,单元不能独立运行,需要我们实现驱动代码,它的这个特点决定了实现单元测试自动化是非常容易也是顺理成章的事。

    3      Java单元测试技术总览

    基于Java的开发一般分为Java应用程序开发Web应用程序开发,目前我们已经有了针对Java开发的较为全面的单元测试解决方案,可以说,每个开发领域都已经有了相应的单元测试技术支持,以下是一个简单示意图,后面的章节我们针对每项技术分别阐述。

     

    4      单元测试的基础框架(JUnit)

    JUnit是Java单元测试的经典之作,它的功能包括:提供实现测试用例的框架,并驱动测试执行;提供验证逻辑;用例执行结果统计与报告;测试套、测试用例管理。JUnit是Java单元测试的基础框架,Java领域的其它测试工具和技术一般都是基于它的扩展,如我们后面提到的:EasyMock、StrutsTestCase、DBUnit等,另外Spring中的单元测试支持也是基于它的。

    使用JUnit只需要将junit.jar加入CLASSPATH路径即可。因为Java编译后不需要链接(不同于C++),加上流行的IDE,如Eclipse、JBuilder、NetBeans等,都集成有JUnit,我们可以在IDE中边写代码、边做测试、边做重构。所以实际上,Java的单元测试比起C++来更方便。

    1)        基本概念

    测试用例(TestCase):同我们公司的测试用例概念,但用例是以测试方法(测试代码)的形式体现的,一个测试方法对应一个测试用例,JUnit框架提供了抽象类TestCase,我们要做的就是继承该类,增加测试方法,在测试方法中实现对被测试代码的调用,并增加验证点。同一个类可以有很多个测试方法,你只要向测试框架提供这个实现类就可以了,框架负责生成测试用例对象(就是实例化测试用例),一个测试方法生成一个测试用例对象,用例有用例名称,取的就是测试方法名。测试方法的原型(函数定义)必须满足以下要求:

    ü  当然是公共方法

    ü  方法名以“test”开头

    ü  方法无参数

    ü  方法的返回值类型为void

    框架在装载你的测试用例类的时候,使用JAVA的反射技术,基于以上条件找到你的测试方法,并创建用例对象。

    测试套(TestSuite):若干个测试用例对象组成一个测试套,测试套与测试用例的关系就如同文件夹与文件的关系一样,测试套包含测试用例,当然也可以包含子测试套。之所以这样,是便于测试用例的组织和管理。JUnit提供了可直接实例化的TestSuite类,实现了很多功能,例如:分析一个测试用例类,对每一个测试方法生成一个测试用例对象,将这个测试用例类的所有测试用例对象作为一个测试套;实现对测试用例的调用以及子测试套的递归调用(实际上是子测试套包括的用例)。

    2)        测试用例类的基本结构

    以下是测试用例类的基本结构:

    public class HelloWorldTest extends TestCase

    {

     

        /*

         * 每次用例执行前要执行的初始化方法

         */

        protected void setUp( ) throws Exception

        {

            super.setUp();

        }

     

        /*

         * 每次用例执行后要执行的清除功能

         */

        protected void tearDown( ) throws Exception

        {

            super.tearDown();

        }

     

        /*

         * 一个测试方法,在其中实现对被测单元的调用,并验证

         */

        public final void testCalculate( )

        {

            //TODO 实现 calculate()

        }

     

    }

    其中TestCase基类由JUnit框架提供。

    3)        框架常见类介绍

    JUnit包含6个包(package):junit.awtui、junit.swingui、junit.textui、junit.extensions、junit.framework、junit.runner。其中前三个包中包含了JUnit运行时的入口程序以及运行结果显示界面,它们对于JUnit使用者来说基本是透明的。junit.runner包中包含了支持单元测试运行的一些基础类以及自己的类加载器,它对于JUnit使用者来说是完全透明的。剩下的两个包是和使用JUnit进行单元测试紧密联系在一起的。其中junit.framework包含有编写一般JUnit单元测试类必须要用到的JUnit类;而junit.extensions则是对framework包在功能上的一些必要扩展以及为更多的功能扩展留下的接口。我们常用的还是junit.framework包,所以下面我就将这个包下的类作一介绍。

    以上是这个包下的类图,为方便理解,我将Throwable类和Error类也放了进来,注意它们是JDK提供的类,非本包的类。

    u  接口Test、抽象类TestCase、类TestSuite之间的关系

    Test是接口(interface),定义有方法run,TestCase和TestSuite都实现了该接口,前面我们提到TestSuite包含TestCase, TestSuite有一个Vector成员,Vector的元素就是它包含的TestCase,当然也可以是子TestSuite,所以为便于管理,TestSuite只认Test接口,在它看来TestSuite包含的是Test对象,而不用细分为TestCase和子TestSuite。对测试套run方法的调用,相应地就会递归调用它所包含的用例run方法和子测试套run方法。这里使用到了Composite设计模式,另外,从命令调用(run命令)与命令实现分离的角度看又能看到Command设计模式的影子。

    u  抽象类TestCase

    抽象类TestCase实现了接口Test,以下是接口Test的一个方法原型:

    public abstract void run(TestResult result);

    TestCase对这个方法的实现如下:

    public void run(TestResult result) {

         result.run(this);

    }

    对这个方法的调用又将主动权交给TestResult,由TestResult来执行对被测方法的调用,这样便于TestResult跟踪用例执行结果并记录下来,以便作最后的统计分析。

    转到TestResult中这个方法的实现:

    protected void run(final TestCase test) {

         startTest(test);

         Protectable p= new Protectable() {

            public void protect() throws Throwable {

                test.runBare();

            }

         };

         runProtected(test, p);

     

         endTest(test);

         //这个方法和上面的startTest用于调用TestListener接口对象,实现监听机制。

    }

    在这个方法中调用了TestCase的runBare方法:

    public void runBare() throws Throwable {

         setUp();

         try {

            runTest();

         }

         finally {

            tearDown();

         }

    }

    可以看到先调用了setUp,之后不管是否产生了异常,都一律调用了tearDown,这两个方法在TestCase中空实现,我们的用例子类可以覆盖(overload)这两个方法,用于用例执行前的初始环境建立和用例执行后的环境清除。接下来我们关注TestCase的runTest方法,看它如何调用到我们的测试方法的。前面提到,TestCase维护了一个用例名称,这个用例名称在TestCase实例化时传入,用例名称就是测试方法名,知道了测试方法名,也就知道了测试方法原型,使用JAVA的反射机制实现了对测试方法的调用。注意这个测试方法正是TestCase子类需要实现的。

    protected void runTest() throws Throwable {

            ......

         Method runMethod= null;

         try {

            runMethod= getClass().getMethod(fName, null);// fName是测试方法名

         } catch (NoSuchMethodException e) {

            ......

         }

         ……

         try {

            runMethod.invoke(this, new Class[0]);

         }

            ......

    }

    u  类TestSuite

    类TestSuite不需要我们再子类化了,它维护了对一组测试用例对象的引用(包括它所包含的嵌套TestSuite对象),它也实现了Test的接口,run方法的实现就是递归调用它所引用的对象的run方法,下面我们谈一下给定一个TestCase子类(指类定义),TestSuite是如何针对其中定义的每个测试方法生成对应的TestCase对象的。以下是TestSuite的两个相关方法,一个是构造方法(给定用例类,生成该测试套下的用例对象)和成员方法(给定用例类,生成子测试套下的用例对象)。

    public TestSuite(final Class theClass){ }

    public void addTestSuite(Class testClass) { }

    这两个方法的实现相似,基本实现思路是根据JAVA的反射技术,找出用例定义的满足以下条件的成员方法(测试方法)

    条件1:当然是公共方法

    条件2:方法名以“test”开头

    条件3:方法无参数

    条件4:方法的返回值类型为void

    对应每一个这样的方法生成该用例类的一个对象,并将用例名初始化为该方法名。测试套下的用例对象就是这样生成。

    u  类Assert

    类Assert是TestCase的基类,其中定义了可供TestCase使用的大量静态方法,用于实际返回值与预期值的比较,这些方法名都是以assert开头,在我们自己写的测试方法中可以使用这些方法用于测试结果的验证。要注意的是JUnit对测试失败(实际值与预期值不符)是以抛出AssertionFailedError异常的形式体现的。我们看一个assert方法的典型实现:

    static public void assertTrue(String message, boolean condition) {

         if (!condition)

            fail(message);

    }

    static public void fail(String message) {

         throw new AssertionFailedError(message);

    }

    一旦验证逻辑验证失败,该用例就会抛出AssertionFailedError异常,TestResult记录下测试失败的该用例对象引用以及它所抛出的异常,这两者的对应关系是TestFailure类体现的,它用于维护用例对象与异常(注意异常的引用类型是所有异常的基类Throwable)的对应关系。TestResult有一个Vector类型成员变量fFailures,它的元素就是TestFailure对象。

    用例执行中除了可能会捕获到AssertionFailedError异常,被测代码有可能会抛出其它异常,包括RuntimeException运行期异常、JAVA虚拟机抛出的Error异常,抛出了这样的异常,也意味着用例执行失败,为此TestResult还有另一个Vector类型成员变量fErrors,它的元素也是TestFailure对象,不过它维护的是用例对象与非AssertionFailedError异常的对应关系。

    在用例执行结果中分别显示出因抛出AssertionFailedError异常而失败的用例个数(故障,Failure),以及因抛出非AssertionFailedError异常而失败的用例个数(错误,Error)。

    以下是在Eclipse中的的执行结果图示:

    4)        JUnit4.X的用例设计

    JUnit4.0之后的版本在用例设计上有较大的改变,主要使用了JDK5.0的Annotation新特性,但业软有相当多的版本还未升级到JDK5.0,另外,部分基于JUnit的单元测试工具还未升级到JUnit4.X,所以短期内就会出现JUnit新老版本共存的局面,但不管如何,JUnit中设计用例都很方便。下面我们简单看一下JUnit4.0以后的用例设计。

    u  用例类不再需要继承自TestCase基类

    u  测试方法名也不需要必须以test开头

    u  几个Annotation标识

    Ø  @Test—标识一个测试方法(对应一个用例)

    l  expected属性可以标识该方法执行期望会抛出什么异常

    l  timeout属性可以指定该方法必须在指定的时间内执行结束

    l  验证失败抛出JDK自带的AssertionError异常

    Ø  @Before —每个测试方法运行前要执行的方法,相关于以前的setUp()

    Ø  @After —每个测试方法运行后要执行的方法,相关于以前的tearDown()

    Ø  @Ignore —暂时不运行该测试方法

    另外,新版本的EclipseANT已经全面支持JUnit4.X

  • 相关阅读:
    树莓派:2安装MySQL
    树莓派:1组装启动
    NLifeBill第六章月总额报表
    NLifeBill第五章修改页面
    NLifeBill第四章添加页面
    NLifeBill第三章Angularjs使用
    NLifeBill第二章日历显示
    NLifeBill第一章项目搭建
    『ORACLE』 SQL语句简单应用(二)(11g)
    『ORACLE』 SQL语句简单使用(一)(11g)
  • 原文地址:https://www.cnblogs.com/jifeng/p/4659356.html
Copyright © 2011-2022 走看看