原创 运用DBUnit进行高效单元测试收藏

 | 旧一篇: 被动实例化

 

        引入DBUnit

       
         现实系统中通常会有一些具有外部依赖性的对象,这些对象和数据库或者其他对象存在诸多关联。如果我们对这样的对象编写单元和组件级测试的话,可以想象将是非常麻烦的一件事.因为这种外部依赖性的存在,使的我们很难将对象孤立出来进行测试。经常提及的白盒测试法,基本上就是通过控制对象的外部依赖性来达到隔离对象的目的,使的可以操作这些对象的状态和相关行为。

        运用 模拟对象(mock objects)或者stubs,就是一个控制对象外部依赖性的解决方案。通过隔离那些关联的数据库访问类,象JDBC的相关操作类,对于控制对象外部依赖性将是很有效的。但模拟对象的解决方案对一些特殊的应用系统架构就显得力不从心了,象那些运用了EJB的CMP(container-managed persistence)或者 JDO(java Data Objects)的应用系统架构,在这些架构里,数据库的访问对象是在最底层的而且很隐蔽。

        由Manuel Laflamme 编写的开放源代码的DBUnit架构体系,对于控制系统内部的数据库依赖性提供了一个非常不错的解决方案。他允许程序员在整个的测试过程中自由的管理控制数据库的状态,这很重要。利用DBUnit,在测试之前,我们可以给目标数据库植入我们需要的数据集,而且,在测试完毕后,数据库完全能够回溯到测试前的状态。

       
         在很多成功的软件项目中,测试自动化往往是关键的层面。DBUnit允许开发人员创建测试用例代码,在这些测试用例的生命周期内我们可以很好的控制数据库的状态。而且,这些测试用例是很容易实现自动化的。这样在测试过程中我们无须对它进行人工的干预,为人工干预造成的后果而担心就更没必要了。

         简单介绍

       
          配置使用DBUnit的第一步我们首先需要知道如何生成数据库schema,这个文件是XML格式的,其中包括了数据库的表及相关数据信息。


        例如,这里有一个数据库表employee ,我们可以用SQL的形式这样将他表示出来。

                      

        而且,我们可以看到,一个简单的数据集可以这样表示

                  

        在DBUnit中,上面这个表和抽样数据信息可以用XML文件的形式这样表示:

        <EMPLOYEE employee_uid='1'
            start_date='2001-11-01'
            first_name='Andrew'
            ssn='xxx-xx-xxxx'
            last_name='Glover' />

        这个生成的XML格式的文件可以作为系统所需的所有种子文件(seed files)的样本模版.

       
         为相互关联的测试场景创建多个种子文件是一个很有效的策略,就象通过不同的数据库文件来区分隔离数据库状态是一个道理。多种子文件策略可以将我们的测试目标锁定到较小的范围,目标数据可以只针对数据库的表,而不是整个数据库。

        为了给目标数据库植入不同的职员记录,我们需要的XML数据文件如下所示:

        <?xml version='1.0' encoding='UTF-8'?>

        <dataset>
            <EMPLOYEE employee_uid='1'
               start_date='2001-01-01'
               first_name='Drew' ssn='000-29-2030'
               last_name='Smith' />

          <EMPLOYEE employee_uid='2'
             start_date='2002-04-04'
             first_name='Nick' ssn='000-90-0000'
             last_name='Marquiss' />

          <EMPLOYEE employee_uid='3'
            start_date='2003-06-03'
            first_name='Jose' ssn='000-67-0000'
            last_name='Whitson' />
   </dataset>


        现在,要让DBUnit和我们所需的数据库schema一起工作了,对于程序员来说,我们使用DBUnit进行测试可以有两种选择:通过直接编码方式进行测试或者与Ant结合.

    编码方式

        
        DBUnit框架提供了一个基本的抽象测试用例类,叫做DatabaseTestCase,它是JUnit框架中的基础类TestCase的子类。如果我们使用这个类必须首先实现两个钩子方法(hook methods):getConnection()和getDataSet().

        
         方法getConnection()需要返回一个IDatabaseConnection类型的对象,这个对象是一个基于普通JDBC连接的包装类。例如,下面的代码段演示了在MySQL数据库环境下,IDatabaseConnection类型连接对象的创建方法。

    protected IDatabaseConnection getConnection()
     throws Exception {

        Class driverClass = Class.forName("org.gjt.mm.mysql.Driver");

        Connection jdbcConnection = DriverManager.getConnection(
                                             "jdbc:mysql://127.0.0.1/hr", "hr", "hr");

        return new DatabaseConnection(jdbcConnection);
}
       
         方法getDataSet()返回一个IDataSet类型对象,其实,说白了,他就是我们先前提到的XML数据的种子文件的另一种表现形式。

      protected IDataSet getDataSet() throws Exception {
                    return new FlatXmlDataSet(new   FileInputStream("hr-seed.xml"));
      }
       
          有了这两个基本的方法以后,DBUnit就可以按照它预先缺省的行为工作了。DatabaseTestCase类提供了两个fixture(我叫它固件,不知仁兄同意否?)方法来控制测试前和测试后的数据库状态。这两个方法就是:
getSetUpOperation() 和 getTearDownOperation().

       
           一种高效的实施方案就是让getSetUpOperation()方法执行REFRESH操作,通过这个操作,我们可以用种子文件中的数据去更新目标数据库里的数据。接下来,就是getTearDownOperation(),让他去执行一个NONE操作,也就是什么也不执行。

   protected DatabaseOperation getSetUpOperation()
                                                   throws Exception {
          return DatabaseOperation.REFRESH;
   }

  protected DatabaseOperation getTearDownOperation()
                                                 throws Exception {
          return DatabaseOperation.NONE;
  }

        
         还有一种有效的方法就是在getSetUpOperation()方法中执行CLEAN_INSERT操作,这样首先会将目标数据库中与我们提供的种子文件一致的数据删除,然后将我们提供的数据插入到数据库中。这个实施顺序保证了我们对数据库的精确控制。

       代码样例

       
          在一个基于J2EE的人力资源系统中,我们很希望对某个数据操作周期实现测试自动化,这个操作周期包括职员的新增,检索,更新和删除。远程接口定义了下列的业务方法(为了简洁清楚,省略了方法中的throws子句).

//译者注:这里的EmployeeValueObject类型对象,译者认为是代表职员实体信息的对象。

public void   createEmployee( EmployeeValueObject emplVo )

public EmployeeValueObject   getEmployeeBySocialSecNum( String ssn )

public void    updateEmployee( EmployeeValueObject emplVo )

public void    deleteEmployee( EmployeeValueObject emplVo )

       测试getEmployeeBySocialSecNum()方法需要植入一条数据到目标数据库中,另外,测试deleteEmployee()方法和updateEmployee()方法时,同样也是在先前植入的这条记录的基础上进行。最后,测试类会首先利用createEmployee()方法创建一条记录,同时我们需要校验执行这个方法时,是否会有异常发生。

     
下面这个DBUnit种子文件,叫做"employee_hr_seed.xml",下面将用到这个文件。

         <?xml version='1.0' encoding='UTF-8'?>
            <dataset>
                  <EMPLOYEE employee_uid='1'
                           start_date='2001-01-01'
                           first_name='Drew' ssn='333-29-9999'
                           last_name='Smith' />
                 <EMPLOYEE employee_uid='2'
                          start_date='2002-04-04'
                          first_name='Nick' ssn='222-90-1111'
                          last_name='Marquiss' />
                 <EMPLOYEE employee_uid='3'
                         start_date='2003-06-03'
                         first_name='Jose' ssn='111-67-2222'
                         last_name='Whitson' />
           </dataset>

       测试类 EmployeeSessionFacadeTest ,需要扩展DBUnit的基础类DatabaseTestCase并且必须提供对getConnection()和getDataSet()方法的实现,在getConnection()方法中将获得与EJB容器初始化时一样的数据库实例,getDataSet()方法负责读取上面提及的employee_hr_seed.xml文件的数据。

      
        测试方法相当简单,因为DBUnit已经为我们处理了复杂的数据库生命周期任务。为了测试getEmployeeBySocialSecNum()方法,只需要简单的传递一个存在于种子文件中的社保代码号即可,比如 "333-29-9999".

//译者注:EmployeeFacade 类型对象,译者认为是代表底层数据库数据的映射体

public void testFindBySSN() throws Exception{

  EmployeeFacade facade = //obtain somehow

  EmployeeValueObject vo = facade.getEmployeeBySocialSecNum("333-29-9999");

  TestCase.assertNotNull("vo shouldn't be null", vo);
  TestCase.assertEquals("should be Drew", "Drew", vo.getFirstName());
  TestCase.assertEquals("should be Smith", "Smith", vo.getLastName());
}

   
       为了确保操作周期中的创建职员方法createEmployee()没有问题,我们只需简单的执行一下这个方法,然后校验一下看有没有异常抛出,另外,下一步我们要做的就是在这条新增的记录上进行查找操作,看是否可以找到刚创建的记录。

   public void testEmployeeCreate() throws Exception{
      EmployeeValueObject empVo =  new EmployeeValueObject();
      empVo.setFirstName("Noah");
      empVo.setLastName("Awan");
      empVo.setSSN("564-55-5555");

      EmployeeFacade empFacade = //obtain from somewhere
      empFacade.createEmployee(empVo);

     //perform a find by ssn to ensure existence

}

       
       测试updateEmployee()方法包括四步,首先查找我们需要被更新的那条记录,然后更新它,紧接着,重新查找这条记录,确认更新操作是否有效。

  public void testUpdateEmployee() throws Exception{
   
   
EmployeeFacade facade =//obtain facade

    EmployeeValueObject vo = facade.getEmployeeBySocialSecNum("111-67-2222");

    TestCase.assertNotNull("vo was null", vo);
    TestCase.assertEquals("first name should be Jose", "Jose", vo.getFirstName());

     vo.setFirstName("Ramon");

     facade.updateEmployee(vo);

     EmployeeValueObject newVo =  facade.getEmployeeBySocialSecNum("111-67-2222");
     TestCase.assertNotNull("vo was null", newVo);

     TestCase.assertEquals("name should be Ramon", "Ramon", newVo.getFirstName());
}
        
          确保数据操作周期中的删除操作deleteEmployee()的方法和testUpdateEmployee()方法基本类似。它分为三步:首先查找一个已存在的记录实体,然后移除,最后再对相同的记录进行查找,确认这条记录没有被查到。

public void testDeleteEmployee() throws Exception{

   EmployeeFacade facade = //obtain facade

   EmployeeValueObject vo = facade.getEmployeeBySocialSecNum("222-90-1111");

   TestCase.assertNotNull("vo was null", vo);

   facade.deleteEmployee(vo);

  try{

     EmployeeValueObject newVo =  facade.getEmployeeBySocialSecNum("222-90-1111");

     TestCase.fail("returned removed employee");

     }catch(Exception e){
          //ignore
     }
}

      
       上述这些测试代码很简单也很容易理解.因为这些代码唯一的职责就是测试,已经完全从系统程序代码中独立出来,这使测试变的简单。并且,这些测试用例的自动化也很容易实现。

       与Ant的结合

       
       相对于扩展DBUnit中的基础类DatabaseTestCase,DBUnit框架中自带Ant功能,允许我们可以在Ant的build.xml文件中控制数据库的状态.这个功能是相当强大的,因为对于作成的诸多测试用例,它提供了一个相当简洁的解决方案。比如。在Ant中运行JUnit测试,就象下面定义一个任务一样简单明了。

<junit printsummary="yes" haltonfailure="yes">
  <formatter type="xml"/>
  <batchtest fork="yes"
           todir="${reports.tests}">
    <fileset dir="${src.tests}">
      <include name="**/*Test.java"/>
    </fileset>
  </batchtest>
</junit>


DBUnit任务过程中,为了在Junit任务前后控制数据库的状态,我们需要创建一个"setup"操作,在这个操作中种子文件中的数据内容会被插入的数据库中。


<taskdef name="dbunit"
    classname="org.dbunit.ant.DbUnitTask"/>
<dbunit driver=" org.gjt.mm.mysql.Driver "
        url=" jdbc:mysql://127.0.0.1/hr "
        userid="hr"
        password="hr">
    <operation type="INSERT"
            src="seedFile.xml"/>
</dbunit>

然后,还需要一个"tear down"操作,在这个操作中,"setup"操作插入的记录被从目标数据库中删除了。

<dbunit driver=" org.gjt.mm.mysql.Driver "
        url=" jdbc:mysql://127.0.0.1/hr "
        userid="hr"
        password="hr">
    <operation type="DELETE"
           src="seedFile.xml"/>
</dbunit>


       
用上面的代码来包装JUnit任务,能够在批量测试前有效的装载数据到目标数据库中,并且在测试结束后,将已装载的全部数据删除。


<taskdef name="dbunit"
         classname="org.dbunit.ant.DbUnitTask"/>

<!-- set up operation -->
<dbunit driver=" org.gjt.mm.mysql.Driver "
        url=" jdbc:mysql://127.0.0.1/hr "
        userid="hr"
        password="hr">
    <operation type="INSERT"
          src="seedFile.xml"/>
</dbunit>

<!-- run all tests in the source tree -->
<junit printsummary="yes" haltonfailure="yes">
  <formatter type="xml"/>
  <batchtest fork="yes" todir="${reports.tests}">
    <fileset dir="${src.tests}">
      <include name="**/*Test*.java"/>
    </fileset>
  </batchtest>
</junit>

<!-- tear down operation -->
<dbunit driver=" org.gjt.mm.mysql.Driver "
        url=" jdbc:mysql://127.0.0.1/hr "
        userid="hr"
        password="hr">
    <operation type="DELETE"
          src="seedFile.xml"/>
</dbunit>


  结论

       
        能够在测试周期内管理数据库的状态,DBUnit框架的这个功能特性使得测试用例代码的创建和应用的周期大大缩短。而且,通过控制数据库这个主要的依赖对象,使的利用DBUnit框架的的测试更容易自动化。

       
        DBUnit精妙的设计,使的学习使用它变得很简单。在你的测试方案中,如果你能够正确的使用它,那么带来的将是代码稳定性方面的大幅度增强,当然还会使你的开发团队信心倍增。

 

发表于 @ 2004年09月06日 09:00:00|评论(loading...)|编辑

 | 旧一篇: 被动实例化

评论

#roshui 发表于2004-10-16 10:57:00  IP:
TrackBack来自《今日阅读10.16——Daliy Build Collections》

Ping Back来自:blog.csdn.net
#bluevagrant 发表于2004-09-07 13:41:00  IP: 222.248.64.*
Good!
#Raistlin 发表于2004-09-07 14:14:00  IP: 211.162.77.*
看不到图
#尊姓大名 发表于2004-09-08 13:14:00  IP: 218.9.174.*
看不到
#Dibov 发表于2004-09-08 09:05:00  IP: 61.187.87.*
我也看到不图!
#leo 发表于2004-09-08 17:21:00  IP: 202.118.4.*
我感觉DBUnit的作用主要是在测试中维护数据库的状态,如果我的数据访问对象(比如EmployeeFacade)是通过容器获得datasource,还是无法进行单元测试,不知道大家是怎么做的?
#leo 发表于2004-09-09 13:03:00  IP: 202.118.4.*
数据库的表如果没有主键,用DBUnit插入数据将会失败
#cityseabreeze 发表于2004-09-10 09:54:00  IP: 61.152.132.*
DBUnit 的适用对象是谁?
developer, qa or end user?
#lizard 发表于2004-09-10 10:32:00  IP: 221.217.157.*
逻辑是不能测试的,他只能通过状态来证明你的逻辑是对的。
#haha  发表于2004-09-10 10:39:00  IP: 221.217.157.*
当你插入数据时,如果第一条的纪录有为空的字段,就会出错。所以,总是要保证第一行描述包含表中的所有列。
#goodsogn 发表于2004-09-15 09:11:00  IP: 211.147.213.*
请问有没有这样一个工具,可以往数据库中批量插入测试数据。
最简单的
学号(number) 姓名(name)
我只要指定number从043200-043256
姓名随机8位字符,就可以在数据库中插入所需的记录
#WindowsDNA 发表于2004-09-15 14:40:00  IP: 202.99.60.*
运用DBUnit制作你需要的数据不是很简单吗?
http://dbunit.sourceforge.net/faq.html#extract
#WindowsDNA  发表于2004-09-15 14:42:00  IP: 202.99.60.*
你可以为你的XML文件指定DTD来解决
http://dbunit.sourceforge.net/components.html#FlatXmlDataSet
#yoshiyan 发表于2004-09-26 20:00:00  IP: 221.232.82.*
请问一下dbunit导出的数据库文件,乱码问题如何解决?

如果使用默认字符集"UTF8"的话,导出的中文为乱码,请问你是如何解决的.

谢谢先
#database-girl 发表于2004-10-10 14:13:00  IP: 218.108.29.*
如果我想用Dbunit测试数据库的状态,例如当前数据库是否可以连接上?可否查出数据来?暂时并不要求保证查出的数据是否正确。
#database-girl 发表于2004-10-12 19:53:00  IP: 218.108.31.*
Sager 我也在作ant+DbUnit的东东,我看了你的问题,想和你探讨一下,能不能留个联系方式,例如qq,email之类的!
#Sager 发表于2004-10-12 18:12:00  IP: 219.238.147.*
问题链接:http://spring.jactiongroup.net/viewtopic.php?t=907
#mig15 发表于2004-12-16 16:56:00  IP: 218.0.176.*
fixture(我叫它固件,不知仁兄同意否?)
fixture--在XUnit里一般译作"装备"
发表评论  


当前用户设置只有注册用户才能发表评论。如果你没有登录,请点击登录
Csdn Blog version 3.1a
Copyright © windowsDNA