原文:JPA implementation patterns: Testing
作者:Vincent Partington
出处:http://blog.xebia.com/2009/07/11/jpa-implementation-patterns-testing/
在JPA实施模式这一序列的前一篇文章中,我谈到了三种默认的使用JPA来映射继承层次体系的方式,并介绍了一种非标准的、但非常有用的方法,本周我则会讨论各种测试JPA代码的方法。
测试什么?
第一个要问的问题是:我们想要测试哪些代码?在谈论JPA的时候,我们涉及到了两类对象:领域对象和数据访问对象(DAO),从理论上讲,领域对象并不依赖JPA(它们是POJO,对吧?)因此,你可以在没有JPA提供程序(JPA provider)的情况下测试它们的功能,关于这方面,在这里没有什么值得一谈的。不过,在实践中,至少会使用JPA注释来注解领域对象,可能还会加入一些代码来管理(延迟的)双向关联、主键或者序列化的对象,这下子,事情变得更有趣了起来……
(即使这样的JPA特定代码违反了领域对象的POJO性质,但它是必需存在的,以便使得领域对象总是以相同的方式工作,无论是在JPA容器的内部还是外部,双向关联的管理以及使用UUID作为主键都是这一方面的很好的例子,在任何情况下,这都是最可肯定需要测试的代码。)
当然,我们还需要测试DAO,对吧?这里就立即出现了这样一个有趣的问题:我们为什么要测试DAO?它们中的大多数只不过是把工作委托给了JPA提供程序,而测试JPA提供程序显然没有什么意义,除非我们是在编写“学习用的测试”(亦见Robert Martin的干净的代码),或者是在开发自己的JPA提供程序。不过,DAO和领域对象的JPA特有部分之间的联合是应该要测试的。
测试的依据是什么?
既然知道要测试什么了,我们就可以决定依据什么来进行测试。由于是在测试数据库代码,所以我们会希望测试装置中包括一个数据库,该数据库可以是一个内置的内存数据库,如HSQLDB(仅使用内存模式)等,或者是一个“真正的”数据库,例如MySQL或者Oracle等。使用内置的数据库有一个很大的好处,那就是容易安装;不需要执行测试的每个人都运行一个MySQL或者Oracle的实例。不过,如果你的产品代码是运行在其他的数据库之上的话,那么通过这种方式你可能不能够捕捉到所有的数据库问题,因此,针对真正的数据库来进行集成测试还是需要的,但是更多的是在那之后再进行。
对于大多数的测试来说,我们需要的不仅仅是数据库,在测试之前,我们需要正确地设置它,在测试之后,则需要使它处在一种可用的状态中,以便于下一个测试的运行。在运行测试之前安装模式(schema)和使用正确的数据来填充数据库并不是那么难于做到(亦作为一个练习留给读者),不过,在测试之后使数据库返回到一个可用的状态则是一个较困难的问题,我找到了几种解决这一问题的方法:
Spring Framework框架包含了一个使用事务的测试框架来管理测试装置的状态,如果你使用了@Transactional来注释了测试的话,那么SpringJUnit4ClassRunner在每个测试开始之前会启动一个事务,并在测试结束时回滚该事务,返回到一个已知的状态。如果你仍在使用JUnit 3.8的话,你可以扩展AbstractTransactionalSpringContextTests这一基类以达到相同的效果,这看起来好像很不错,不过在实际中,我发现这一方法基于某些理由可以说是不够理想的:
1. 默认情况下,在事务被提交或者有查询被执行之前,JPA上下文都不会被刷新,因此,除非你的测试包括了查询,否则任何修改实际上都没有被传播到数据库中,而这可能会隐匿掉了无效映射等之类的问题。你可以在测试结束之前试着显式地调用EntityManager.flush,不过随后的测试就不再是代表了实际状况的了。
2. 此外,在同一个会话(session)中保存一个实体然后再检索它并不能够查出那些讨厌的延迟加载问题,在JPA提供程序将会返回一个到你刚保存的对象的引用的时候,你可能甚至连数据库都没有碰一下!
3. 最后要说的是,在测试中,你可能想先保存一些数据在数据库中,然后再运行测试,最后再检查正确的数据被写到了数据库中。为了测试这一过程,你可能需要三个单独事务,而且前面的两个事务不能被回滚。
如果使用内置的内存数据库的话,那么在运行第一个测试的时候,数据库是干净的,所以你不必担心在所有测试运行之后如何使之处在一个好的状态中,这就意味着你不需要回滚任何事务,并可以在一个测试中使用多个事务。不过你可能需要在测试与测试之间做一些特别的事情,例如,在使用Spring TestContext框架的时候,在上下两个测试之间,可以使用@DirtiesContext这一注解来重新初始化内存中的数据库。
如果不能够使用内存数据库,或者在每个测试之后都要重新初始化它的代价太昂贵的话,那么可以尝试在每个测试之后(或每个测试之前)都清除所有的表。例如,可以使用DbUnit来从测试表中删除所有的数据或是删去所有的测试表,不过外键约束有可能是个妨碍,所以在执行这些操作之前,你要暂时使参照完整性失效。
测试的范围是什么?
接下来的事情是确定测试的范围,不知你编写的是小的单元测试、较大的组件测试还是全面的集成测试?由于JPA的工作方式(可以说是它的抽象泄漏),一些问题可能只会在更大的范围中才表露出来,如果想了解基本的正确情况的话,虽然孤立地测试DAO的持久方法是有用的,但是需要在更大的范围之内进行测试,以便能够筛出那些有处理错误的延迟加载或者事务。要真正的测试系统的话,你需要通过较大的组件测试来组合小的单元测试,在这些组件测试中,把服务门面和DAO绑定测试。这两类测试都可以使用内存数据库来进行。要完全覆盖测试的范围的话,你需要以生产中使用的数据库来进行集成测试,可使用Fitnesse之类的工具,因为我们并没有专门在该种情况下测试JPA代码,所以规模较小的单元测试会较快地帮助你找出DAO的错误。
断言什么?
最后一件要解决的事情是在测试中断言什么?有可能是这样的情形,领域对象被映射到了现有的模式(schema)上,你想要确保这一具体情况中的映射是正确的,在这种情况下,你会想用原始的JDBC来访问底层的数据库来明确肯定正确的修改已经被写到正确的表中。不过,如果模式是由JPA映射来自动生成的话,那么你可能不会关心实际的模式。你想要明确肯定持久的对象能够在新的会话中内正确地检索出来,使用JDBC来直接地访问底层的模式是没有什么必要的,而且只会使得这样的测试代码变得脆弱。
很肯定在这篇博文中我并没有涵盖了所有的测试场景,因为测试数据库代码是一个非常复杂的领域,我很愿意知道你是如何测试数据库代码的,无论是使用JPA或者其他的一些持久性机制都可。