用DbUnit进行数据库集成测试1引言JUnit家族为Java应用程序的单元测试提供了基本框架,除JUnit之外,还有许多用于专门测试领域的其他工具和框架,本章将介绍用于数据库集成测试的重要工具DbUnit(注1)。这里将数据库测试描述为“集成测试”,以便与普通“单元测试”相区分,集成测试涉及用户自身代码之外的架构,在数据库集成测试的情况下,额外的架构是真实数据库。DbUnit通常称为“JUnit扩展”,它提供了TestCase子类,用户可以在自己的测试类中扩展它们,但也可以独立于JUnit以其他方式来使用DbUnit。例如,可以从Ant中调用DbUnit执行某些任务。本章将说明DbUnit的主要用途,并提供一些典型用法的简单示例,然后还将继续介绍一些其他相关主题。2概述DbUnit的用途DbUnit有两种主要用途:准备数据库DbUnit可以在执行每个测试方法前用已知内容设置表。验证数据库DbUnit可以在执行每个测试方法后更方便地验证表的内容。如果不使用DbUnit,用户还有一些其他选择,不过这些选择有些难以使用。用户可以手工编写JDBC调用来准备和验证数据库,但这通常很难编写,并且需要做大量工作;另外,如果在主应用程序中使用对象-关系映射(ORM),有人会建议通过它来用测试数据填充数据库,以及从数据库中读取数据值进行验证,这相当于用自己的数据访问层来测试自己的数据访问层,它可能不是个好主意。ORM技术涉及相当多的技巧,例如缓存,用户最好不要使用它,以免测试失败。与此相对照,DbUnit提供了一种相对简单灵活的方式来准备和验证数据库,这种方式独立于被测代码。设置DbUnit要使用DbUnit,首先需要从文件,将此JAR文件放在集成开发环境或构建脚本的类路径中,在类路径上还需要有数据库的JDBC驱动程序,除此之外,对于基本的DbUnit应用不需要其他依赖项。用户还应该阅读上的在线文档。接下来要决定调用DbUnit的方式。对于大多数测试场景,用户将在自己的测试类中直接或间接调用DbUnit,后面将要看到,也可以从Ant中使用DbUnit来执行某些任务。用DbUnit运行测试在用DbUnit进行数据库测试时是在对真实数据库进行测试,用户在运行测试时需要连接到真实数据库。由于DbUnit和被测代码将在此数据库中插入和修改数据,因此每个开发人员都需要或多或少地拥有对数据库或者至少是其中的模式(schema)的独占访问权限,否则,如果不同的开发人员同时运行测试,他们可能相互冲突。理想的设置是,每个开发人员在自己的计算机上安装数据库软件,现在的计算机功能足够强大,可以用很小的开销来运行几乎任何数据库软件。用户不必在自己的计算机上拥有完整的生产数据库副本,在数据量相对小的情况下,DbUnit测试工作得最好,它们通常测试系统功能是否正确,而不是测试系统性能。有人建议将轻量级的嵌入式数据库(如HSQLDB或ApacheDerby)用于运行集成测试,这样做的好处是,不需要在自己的计算机上安装特殊数据库软件,另外,使用嵌入式数据库运行测试的速度通常比真实数据库更快,因为可以在内存中运行大多数测试,并且一般开销更小。然而,对于重要的应用程序,这通常是误导。应用程序往往包含数据库特有的代码或使用数据库特有的功能,除非在运行测试时使用相同的数据库,否则,无法对这些代码和功能进行正确测试。另外,即使是使用ORM层,如Hibernate或JPA,这似乎使代码独立于数据库,但ORM生成的实际SQL代码也可能因数据库不同而异,JDBC驱动程序无疑也不一样。因此,嵌入式数据库与生产数据库之间存在着显著的功能差异,要使测试尽可能有用和有效,应该努力使数据库环境相同。当然,如果生产系统本身使用嵌入式数据库会更好。JUNIT下载地址:当DatabaseTestCase或IDatabaseTester需要访问实际数据库时,它通过IDatabaseConnection接口进行访问。IDatabaseConnection本质上是一个JDBCConnection的包装程序或适配器,可以通过DatabaseConfig类来定制IDatabaseConnection,DatabaseConfig是一组“名称/值”形式的功能和属性,图14-2给出了这些类。IDataSetDbUnit将用于测试的实际数据表示为IDataSet,它是一组表中的数据的抽象表示。图14-2:IDatabaseConnection和DatabaseConfigDbUnit提供了IDataSet接口的几个具体实现,可以使用它们从不同数据源获取数据集以及为它们提供额外功能。图14-3中给出了部分实现,例如,FlatXmlDataSet、CsvDataSet和XlsDataSet是表示文件中数据的不同方式,后面将看到这些实现的例子,通过查看源代码,还可以找到其他实现形式。3.DbUnit的结构(1)开始使用DbUnit的最简单方式是扩展其提供的某个基类,不过,在后面的例子中将会看到,这不是必需的或并不总是可取的。首先介绍这些基类。与任何好的面向对象框架或程序库一样,DbUnit包含了大量接口,大多数功能都由这些接口来规定。DbUnit采用的约定是,接口以大写字母I打头。DbUnit通常提供了接口的一些具体实现,典型情况下,将有一个抽象基类实现以及多个特定实体子类。DatabaseTestCaseDbUnit中的主要基类是DatabaseTestCase。在DbUnit2.2之前,用户应扩展Data-baseTestCase来创建自己的测试类,从DbUnit2.2开始,应该扩展一个新的子类DBTestCase。DBTestCase的主要不同之处在于,它提供了一个委托给IDatabase-Tester接口的getConnection()方法,可以通过改写newDatabaseTester()来提供或改写默认的IDatabaseTester。IDatabaseTester负责提供一些重要的测试功能:数据库连接。设置测试数据集。setup(设置)操作(通常是CLEAN_INSERT)。teardown(清理)操作(通常是none)。DbUnit提供了一些标准IDatabaseTester实现,用户可以方便地使用和扩展这些实现,或者提供自己的实现。DatabaseTestCase和IDatabaseTester的层次结构如图14-1所示。在后面的示例中,将会详细介绍这些类的工作情况,在说明相关例子时将参考这些图中的功能。3DbUnit的结构(2)ITable表用ITable接口表示,ITable本质上是一组行和列,图14-4中给出了ITable及其部分实现。图14-4:ITable及其部分实现ITableMetaData最后,DbUnit使用ITableMetaData对象提供关于表的信息:表的名称和列的特征,图14-5给出了ITableMetaData及其部分实现。这些接口代表了DbUnit的核心功能,当然,DbUnit包含的接口和类远不止这些,在后面的例子中,将会看到一些其他接口和类。要开始使用DbUnit,并不需要理解所有这些类,某些类如ITableMetaData只会在自定义或扩展DbUnit时才会用到,通过后面的例子,将很好地理解这些类是如何相互配合以及如何在项目中使用它们。4示例应用程序下面将通过一系列例子来说明各种DbUnit功能,本书所附源代码中提供了这些示例的代码,同时还提供了设置和运行这些代码的Ant构建脚本。对于下面的例子将使用改编自Spring框架(注2)的PetClinic应用程序的数据库模式,此应用程序是Spring框架提供的一个示例,它实现了经典的PetStoreJ2EE演示程序。这里的大多数例子不依赖Spring框架——我们只是将此数据库模式用作一个方便的示例应用程序。图14-6中给出了将要使用的PetClinic数据库模式,这里对原始版本略微进行了修改,在其中添加了一些列,以便在演示特定DbUnit功能时使用。此应用程序的Spring版本提供了针对HSQLDB和MySQL数据库的实现,对于本书中的示例,将在Oracle上运行该模式(可以为Windows或GNU/Linux免费下载OracleExpressEdition),create_tables.sql文件中提供了定义该模式的OracleSQLDDL。5准备数据库(1)开始的示例将演示如何用DbUnit在测试前用已知的测试数据来准备数据库,这样做的标准方式是使用测试的setUp()方法,或者,更准确地说,是DatabaseTestCase的setUp()方法。基本想法是用户提供包含所需数据的IDataSet,DbUnit加载数据。在开始的示例中将使用FlatXmlDataSet数据集,它可能是最常用的IDataSet实现,后面将给出一些其他数据集格式。验证对单行数据的查询最简单的数据库测试是对从数据库中检索单行数据的代码进行测试。考虑owners表的这个数据访问对象(DAO,DataAccessObject)接口:1.publicinterfaceOwnerDao{2.CollectionOwnerfindOwners(StringlastName);3.OwnerloadOwner(intid);4.voidstoreOwner(Ownerowner);5.voiddeleteOwner(Ownerowner);6.}此DAO包括了一个loadOwner()方法,该方法按ID检索单个Owner,以下代码给出了它的一个简单JDBC实现:1.publicclassJdbcOwnerDaoextendsAbstractJdbcDaoimplementsOwnerDao{2.//...3.publicOwnerloadOwner(intid){4.Connectionconn=null;5.PreparedStatementstmt=null;6.PreparedStatementstmt2=null;7.ResultSetrs=null;8.try{9.conn=getConnection();10.stmt=conn.prepareStatement(11.SELECTid,first_name,last_name,address,city,telephone12.+FROMownersWHEREid=?);13.stmt.setInt(1,id);14.stmt2=conn.prepareStatement(15.SELECTp.idp_id,p.namep_name,p.birth_date,t.idt_id,16.t.namet_name17.+FROMpetspJOINtypestONt.id=p.type_idWHEREowner_id18.=?ORDERBY1);19.stmt2.setInt(1,id);20.rs=stmt.executeQuery();21.if(rs.next()){22.Ownerresult=newOwner();23.result.setId(id);24.result.setFirstName(rs.getString(first_name));25.result.setLastName(rs.getString(last_name));26.result.setAddress(rs.getString(address));27.result.setC