iBATIS中的单元测试

DAO消费者

持久化逻辑

     单元测试已经成为了现代软件开发方法中的一个非常重要的组成部分。即使不赞成极限编程(eXtreme Programming, XP)或者其他敏捷方法能够带来好处,单元测试也应该成为你的软件开发生命周期中的一个基础实践。
从概念上说,持久层可以分为3层,而iBATIS使得对这些不同的层进行单元测试都变得非常简单,如图13-1所示。

SQL映射

  • BATIS至少在以下3个方面可以使得你对这些不同层进行单元测试变得更容易:
  • 测试映射层(mapping layer)本身,包括各个映射、所有的SQL语句,以及这些SQL语句被映射到的那些领域对象。
  • 测试DAO层,这使你可以对DAO层中的任何特定于持久化的逻辑进行测试。
  • 在DAO的消费层中进行测试。

13.1.1 对映射层进行单元测试
      对映射层所进行的单元测试,可能是在大部分应用程序中所发生的最低层次的单元测试了。此过程包括对SQL语句以及这些语句所映射到的领域对象进行测试。这意味着我们将需要一个用于进行测试的数据库实例。

2 1. 测试用数据库实例

     测试用数据库实例可能是创建于你实际使用的数据库管理系统(例如,Oracle或者微软的SQL Server)中的一个真实实例。如果你的环境对于单元测试是友好的,那么只需要简单地更改一下配置就可以运行单元测试了。如果打算使用非标准数据库特征 (例如,存储过程),那么就可能有必要使用真实的数据库实例。存储过程和其他一些非可移植的数据库设计时选择,会使得对数据库进行单元测试变得很难,除非 使用真实的数据库实例。
      使用真实的数据库实例的缺点是,只有连接到网络才能进行单元测 试。或者也可以使用某个真实数据库的一个本地实例,但这意味着单元测试在运行之前将需要额外的本地环境设置。无论使用这两种方法中的哪一种,你都将面对同 一个问题,即每次测试都必须重建测试数据,甚至可能需要重建测试套件(test                           suite)之间的模式,或者是每个单元测试之间的模式。即使是在大型的企业级的数据库服务器上,要完成以上任务也需要花费很长时间。另一个问题是,由于 使用的数据库是集中式的,多个开发人员同时进行单元测试时就可能会导致冲突。所以,必须使用不同的数据库模式来隔离每一个开发人员。正如你所见,这种方法 的普遍问题就是,单元测试取决于相当多的基础设施,而这对于大多数经验丰富的测试驱动程序的开发人员来说是不够完美的。
     Java开发人员是非常幸运的,因为他们至少有一种非常棒的内存 (in-memory)数据库可以使用,这种数据库可以使得对相对标准的数据库设计进行单元测试变得非常简单。HSQLDB是一个完全用Java写成的内 存数据库。它既不需要磁盘上的任何文件也不需要连接网络就能够正常工作。此外,它还能够重新生成来自典型数据库(例如Oracle和微软的SQL Server)的大部分数据库设计。即使由于设计过于复杂(例如使用了存储过程)而导致HSQLDB不能重建整个数据库,它也仍然能够重新生成该数据库的 绝大部分。HSQLDB允许快速重建数据库,包括数据库模式和测试数据。iBATIS自己的单元测试套件就是使用HSQLDB在每个单独的测试之间重建数 据库模式和测试数据。我们亲自使用HSQLDB测试了由将近1000个数据库相关的测试构成的测试套件,运行时间不到30秒。
      有关HSQLDB的更多信息,请访问网页http://hsqldb.sourceforge.net/。另外,可能会让微软的.NET高兴的一个消息是,已经有人发起对HSQLDB的移植了,同时也有人开始创建其他的内存数据库了。


3 2. 数据库脚本
      现在已经有了数据库实例,那么数据库模式和测试数据应该如何构造和创建呢?你可能已经有了可以用来创建数据库模式以及测试数据的数据库脚本了。理想情况 下,你应该将这些脚本纳入到版本控制系统(例如CVS或者Subversion)中。这些脚本应该和应用程序中的其他代码一样被同等地对待。即使你对自己 使用的数据库没有控制权,你也应该定期从拥有控制权的人那里获得相应的更新。应用程序的源代码与数据库脚本应该始终保持同步,并且单元测试本来就是确保它 们同步的。每当运行单元测试套件时,你还应该运行这些脚本来重新创建数据库模式。使用这种方法,可以很容易地将数据库创建脚本需要的新集提交给版本控制系 统,然后运行单元测试以便确定脚本更新是否给应用程序带来了问题。这是最理想的情况。如果使用内存关系数据库(例如HSQLDB)来运行测试,则可能需要 另外一个步骤来转换数据库模式。可以考虑使这个转换过程自动化,以便避免手工编程可能出现的错误,加快集成的速度。

4 3. iBATIS配置文件(例如SqlMapConfig.xml)
     为了进行单元测试,你可能想要使用一个独立的iBATIS配置文件。配置文件用于控制数据源和事务管理器的配置,它在测试环境和产品环境中可能会完全不 同。例如,产品环境可能会是一个像J2EE应用程序服务器这样的受管理环境。在这样的环境下,一个受管理的DataSource实例可能是从JNDI中检 索得到的。你还可能会在产品环境中利用全局事务。然而,在测试环境中,你的应用程序可能不会运行在服务器中;而只是配置了一个简单DataSource, 使用的也是局部事务。分别进行测试环境配置和产品环境配置的最简单的方式就是,使用不同的iBATIS配置文件,这两份配置文件引用相同的一组SQL映射 文件。

5 4. iBATIS SqlMapClient单元测试
      现在所有的先决条件都已经准备好了,这些先决条件包括数据库实例、自动构建数据库的脚本,以及用于测试的配置文件,接下来可以开始创建单元测试了。代码清单13-1即是一个使用JUnit来创建简单单元测试的例子。
代码清单13-1 SqlMapClient单元测试示例
设置单元测试和测试数据

利用主键值测试单个person对象的检索

     代码清单13-1中的示例使用了针对Java的JUnit单元测试框架。(可以在www.junit.org上找到 更多有关JUnit的信息。对于.NET Framework,也有相似的工具,包括NUnit,可以从网页www.nunit.org上下载得到。)在我们的设置方法中,首先删除了测试涉及的那 些数据库表,然后再重建它们并对它们重新填充测试数据。为每一个测试都重建所有的东西可以确保各个测试间相互独立,但是如果在像Oracle或者SQL Server这样的RDBMS上这样删删建建地做测试那就太慢了。在这种情况下,可以使用类似于HSQLDB这样的内存关系数据库。在我们实际的测试案例 中,我们从数据库中读取一条记录,将其映射到一个bean上,然后断言(assert)bean中各字段的值都是所预期的值。                     

以上就是测试映射层所需做的全部工作了。接下来要测试的层是DAO层,假定你的应用程序有DAO层。

13.1.2 对DAO进行单元测试

      DAO层是一个抽象层,因此根据其本质,DAO应该非常容易测试。DAO也使得对DAO层用户的测试变得更加简单。 本节,将讨论测试DAO本身。DAO通常被分离为一个接口和一个实现。由于我们将直接测试DAO,因此接口将不起作用。我们将直接对DAO实现进行测试。 这可能同DAO模式的工作方式是相反的,但是这恰好体现了单元测试的好处——它把那些坏习惯清除出我们的系统!
      如果可能的话,对DAO层的测试应该不涉及数据库以及底层的基础设施。DAO层是持久化实现的一个接口,但是在测试DAO层时,我们更感兴趣的是测试DAO层内部的东西,而不是测试DAO层之外的东西。
      测试DAO的复杂度仅仅取决于DAO实现。例如,测试一个JDBC DAO可能非常困难。你需要一个很好的模拟框架来代替所有典型的JDBC组件,如Connection、ResultSet和Prepared-   Statement等。即使是这样,要利用模拟对象来管理这样复杂的API也非常麻烦。而模拟iBATIS SqlMapClient接口则简单得多。下面就来试一试。

6 1. 利用模拟对象来对DAO进行单元测试

      模拟对象是指为了进行单元测试而用来替换实际实现的对象。模拟对象通常没有很强的功能性;它们只用于满足某个单一的情况,以使得单元测试仅仅关注其应该关注的部分,而不需要担心复杂度的增加。我们将在下面的例子中使用这些模拟对象来示范一种测试DAO层的方法。
      在我们的示例中,将使用一个简单的DAO。我们将不考虑iBATIS DAO框架,因此就不需要担心事务以及诸如此类的东西了。这个示例的目的就是,示范如何测试DAO层,无论你使用的是哪一种DAO框架(只要确实使用了它)。
      首先,来考察一下要进行测试的DAO。代码清单13-2给出了一个SqlMapPersonDao实现,它调用了一个和13.1.1节中给出的示例相似的SQL映射文件。
      代码清单13-2 测试一个简单的DAO

      请注意在代码清单13-2中我们是如何把SqlMapClient注入到DAO的构造函数中的。这为对DAO进行单元测试提供了一种简易的方式,因为我们 只需要模拟SqlMapClient接口就可以了。显然,这是一个非常简单的示例,没有对其进行太多的测试,但是每一个测试都非常重要。代码清单13-3 显示了用来模拟SqlMapClient并且测试getPerson()方法的单元测试。
代码清单13-3 含有模拟SqlMapClient的PersonDao单元测试

     代码清单13-3中给出的示例使用了JUnit以及JMock这个Java对象模拟框架。正如代码清单13-3中加粗的部分所显示的那样,利用JMock 来模拟SqlMapClient接口实现,使我们能够单独测试DAO的行为,而无需顾虑实际的SqlMapClient实现,也就不需要考虑与其相关的 SQL语句、XML文件,还有数据库了。JMock是一个非常好用的工具,可以在www.jmock.org上找到更多有关它的信息。你可能已经猜到了, 还有一个针对.NET的模拟框架,称为NMock,有关它的更多信息请参考http://nmock.org

13.1.3 对DAO的消费层进行单元测试
      应用程序中那些使用DAO层的其他层称为DAO层的消费者(consumer)。DAO模式使得你可以在不依赖于持久层的任何功能的情况下测试这些消费者 的功能。一个好的DAO实现应该有一个能够很好地描述其可用功能的接口。测试消费层的关键在于获得此接口。考察代码清单13-4中的接口,你就会发现上一 节中所描述的getPerson()方法。
代码清单13-4 简单的DAO接口

      要开始测试DAO层的消费者,所需要的就只是代码清单13-4中所给出的接口。我们甚至根本不需要一个完整的实现。利用JMock,我们能够很轻松地模拟getPerson()方法所预期的行为。考虑如下这个使用了PersonDao接口的服务(见代码清单13-5)。
代码清单13-5 使用PersonDao接口的服务


      我们的单元测试的目标并不是DAO——而是getValidatedPerson()方法中的业务逻辑,例如它所执行的各种验证。方法中的每一个验证可能都是一个私有方法,为了便于讨论,假设此处我们只测试私有接口。
      多亏了之前的PersonDao接口,在没有数据库的情况下测试 getValidatedPerson()方法也很容易。所需要做的只是模拟PersonDao接口实现,然后将该模拟实现传递给服务的构造函数,最后调 用getValidatedPerson()方法即可。代码清单13-6给出了完成上述工作的单元测试。
代码清单13-6 使用模拟而不是真实的DAO,以避免访问数据库

     我们再一次同时使用了JUnit和JMock。正如你在代码清单13-6中所见到的那样,这种测试方式在应用程序的各个层都是一致的。这是很有好处的,因为它可以带来易于维护的凝练的单元测试。      
     有关iBATIS中的单元测试,我们就介绍到这。实际上网上有很多很好的关于单元测试资源。使用Google搜索一下“unit test(单元测试)”,就可以找到很多相关的资源,它们可以帮助你迅速提高单元测试的能力,甚至你还可能发现比本书使用的方法更好的单元测试方法。

posted @ 2011-11-27 14:08  JStar  阅读(3254)  评论(0编辑  收藏  举报