译编者序:
有时编写测试犹如漫漫人生旅途一般,我们常常面对某些抉择,对于如何选择下一步我们要做的,往往决定于我们的视野、价值观。本人翻译、编写此文旨在于为大家提供测试过程中一些参考。因为此文截取自POJOs in Action这本书,所以部分承启的内容不免有些唐突,望读者见谅。
注释:本文摘自POJOs in Action第四章。
注释:文中提及的“数据仓库(database repository):指具体的数据库实现,比如MySql。
正文:
每隔半年,我的牙医Anne-Marie就教育我牙线是如何的重要。每次,我心不在焉地许诺将更加重视,但我从没有坚守诺言。有些开发者对待测试如同我对待牙线的态度:测试对于软件开发是good idea,但是他们不是很被迫的做测试,就是完全不进行测试。
然而,测试作为软件开发进程中的关键组成部分,就如牙线防止牙病一般可以预防软件的“腐败。持久化层和大多数应用组件相似都无法抵御“腐败,所以测试是必需的。你需要编写测试:核对domain model是否正确地映射到database、查询如期地被使用。这里在测试持久化domain model时存在两大挑战。
首先,第一个挑战来自于domain model、ORM文档和数据库schema之间的不一致。例如,一个常见的错误是在映射中忘记定义新增的字段,然而这将引起一些诡异的bug。另一个常见问题是数据库约束阻止应用建立、更新或者删除持久化对象。编写对持久化domain model的测试实质上为了捕捉这些(和其它)问题。
第二个挑战是如何高效的测试持久化domain model从而最小化测试时间。当测试一个大块头domain model的O/R映射时,这个测试suite将花费长时间来执行。不仅大量的测试耗时,而且仅一个访问数据库的测试也比简单的对象测试多花费时间。尽管一些数据库测试是不可避免的,但重要的是能否找到避免这些测试的方法。
本章节,你们将学习不同ORM的bug和如何编写检测它们的测试。在这里我说明了哪些O/R映射测试必须依靠数据库进行测试,而哪些为了最小化执行时间能脱离数据库而单独测试。你们将在第5、6章看到这些方式的测试范例。
4.5.1 O/R测试方式
隐藏于O/R映射中的bug包括以下:
1.映射缺少字段;
2.引用了不存在的表或列;
3.数据库约束阻止持久化对象被插入、更新或删除;
4.无效的查询或返回错误结果;
5.不恰当的数据仓库(repository)实现。
一些bug是由domain model、ORM文档和数据库schema不一致造成的。比如,通过增加新字段或者重命名旧字段的方式修改domain model,而忘记了增加或者更新此字段的O/R映射。某些ORM框架会在缺少字段O/R映射定义时产生错误信息,但另一些(包括Hibernate)却沉默地允许将这个字段作为非持久化类型,这会引发潜在、棘手的bug。同样,在定义字段映射时很容易忘记更新数据库schema。
有一部分bug容易被发现,比如被ORM框架在启动时检测到:Hibernate在应用打开一个SessionFactory时会报告缺少的字段、属性或者对象构造方法。另外一类bug需要指定代码路线才能被执行到。一个对collection字段的错误映射直到应用长时访问collection时才被检测到。同样的,在查询中的bug经常到它被执行时才被检测到。为了发现这种类型的bug,我们必须十分完整的测试应用。
测试持久化层的一个方法是编写依赖数据库运行的测试。例如,我们可以写测试来建立、更新持久化对象、调用数据仓库方法(repository methods)。然而,随之而来的一个问题是测试即使再使用象HSQLDB这样的in-memory数据库也会执行一段时间。另一个问题是测试检测到bug(比如缺少字段映射)时将失败。而且编写这些测试需要花上许多精力。
一个更有效、更快速的方法是使用一些和持久化层分离的测试,即脱离数据库的测试。一些测试依赖数据库,而另一些可以脱离数据库。
这些依赖数据库的测试包括:
1.建立、更新、删除持久化对象的测试;
2.被用于数据仓库的查询测试;
3.验证O/R映射和数据库schema是否匹配的测试。
下面是可脱离数据库的测试:
1.用于数据仓库的假对象(Mock object)测试;
2.通过使用XML映射文档方式验证O/R映射正确性的测试。
下面,我们将看到这些不同的测试。
4.5.2 依赖数据库的测试
依赖数据库的测试是持久化domain model测试中的一个基本组成,尽管它们需要花掉相对长一些的时间执行。目前有两种数据库级别(database-level)测试:第一种是验证持久化对象能被建立、更新、删除。另一种是验证被数据仓库使用的查询。
下面让我们看看每种测试方法。
测试持久化对象
测试持久化domain model的目的在于核对持久化对象是否能保存到数据库中。一个简单的方法是编写测试:建立对象图表,将其保存到数据库。测试并不尝试核对数据表包含正确的值,取而代之的是ORM框架抛出的异常。这种测试是找到基本ORM bug(包括检测是否丢失类映射和数据库列映射)的相对简单方法。尽管这种测试是一种“起步的好办法,但它不能检测其它的ORM bug,比如发生在更新、插入、删除时的约束违背。
我们可以通过编写更加精细的测试来更新、删除持久化对象,从而观察那些“起步测试无法找到的bug类型。例如,一个针对PendingOrder的测试由以下几步构成:
1.建立PendingOrder持久化对象,保存之;
2.读取PendingOrder持久化对象,更新交货信息,保存之;
3.读取PendingOrder持久化对象,更新餐馆,保存之;
4.读取PendingOrder持久化对象,更新订货量,保存之;
5.读取PendingOrder持久化对象,更新订货量,保存之(再次测试删除line items);
6.读取PendingOrder持久化对象,更新支付信息,保存之;
7.删除PendingOrder持久化对象。
上面的测试核对了数据库能够保存对象的所有状态,并在建立或者删除对象之间(PendingOrder与line items)的关联时检测问题。每一步测试由数据库事务构成,此事务使用新的持久化框架建立的数据库连接。通过每次使用新的事务和连接,我们确保了对象真实地被持久化到数据库中并再次被读取。它也确定了对迟缓的约束(因为直到commit时数据库才会检测)是否满足。
这种方式的缺点是它会改变数据库,每次测试前需要初始化数据库到一个已知的状态。
我们还可以通过核对数据表内容来增强这些测试去验证对象字段是否非正确映射:在插入对象到数据库后,测试核对数据库是否包括期待的行、列值。这个测试通过使用JDBC获取数据的方式校对数据库内容。另一种方法是使用DbUnit比较数据表和包含期望数据的XML文件。此方法很精细,而对开发、维护这些测试来讲却是完全乏味的。另外,这些测试不能检测到缺少新增字段或者属性的映射。
总地来看,更好的方法是测试classes和字段/属性映射是否匹配,正如我下面要讲的——直接测试ORM文档。
插入、更新、删除持久化对象的测试完全有用,但是它们对于编写者来讲是不小的挑战:一个原因是一些对象的各种状态需要测试,对于复杂性角度而言另一个原因是测试必需的大量设置(setup)。测试不得不建立被测试对象引用的其它的持久化对象。比如,为了持久化PendingOrder和它的line items,测试不得以地初始化餐厅(Restaurant)和菜单项(MenuItems)。另外,对象的公共接口往往不允许它的字段被直接赋值,所以测试必需使用正确的参数调用一系列业务方法(business methods),也许这些业务方法会调用更多的setup代码。作为后果,这可能对编写良好持久化测试构成了挑战。
这种方法另一个弊端是由于访问数据库所以执行测试很缓慢。每个持久化类可能有多个测试,每个测试由多步组成。每一步多次调用ORM框架,用以执行多个SQL语句。
总而言之,这些测试作为单元测试套件的一部分经常花费太长时间,应把它放到功能测试中。
即使这些持久化对象测试很难编写,并需要很多时间执行,但他们仍作为domain model测试套件中重要的一部分。如果需要,你可以开始编写仅保存对象的测试,然后逐渐添加更新、删除对象的测试。
测试查询
我们需要写数据库级别的查询测试。一个基本的方法是编写仅执行查询、忽略结果的查询。这个快速、简单的方式能使我们捕捉到一些基本错误,这是常常作为测试简单查询而作的。
对于复杂的查询,重要的是检测查询逻辑:比如使用“<代替了“<=。为了捕捉到这类bug,我们需要编写测试装配数据库的测试数据,执行查询,核对返回对象是否为所期待的。不幸的是这种测试需要花费时间编写和执行。
这里有测试执行查询的两个方法:一个选择是使用Spring和Hibernate API直接执行查询,另一个选择是通过调用数据仓库间接执行查询。哪种选择更好依赖多种因素,包括数据仓库的复杂性。如果数据仓库是简单明了的,那么通过调用数据仓库进行查询测试是非常容易的事,因为它直接执行带有一套特定的参数查询。而如果数据仓库比较复杂,那么使用Spring和Hibernate API直接执行查询测试会容易一些。
为了能单独于数据仓库执行测试查询,查询必须要脱离数据仓库被单独保存。最容易实现这种方式的方法是使用定义在映射文档中的被命名的查询(named queries)。Hibernate和JDO 2.0让你在XML映射文档中定义,并提供了一个执行命名查询的API。另外,为了保持查询脱离数据仓库,在XML文档中定义多行查询比在java代码中添加多行字符串要更具可管理性。如果你正使用的ORM框架不支持命名查询,比如JDO1.x实现,则你应该保存查询语句在一个属性文件中。
核对数据库schema是否匹配映射
除非schema是由O/R映射生成的,映射和数据库schema不同步是可能发生的。这很容易,比如在定义字段映射后忘记了增加数据表的一个新列。
我们必须编写核对数据库schema是否与O/R映射是否匹配的测试。
测试数据schema的一个方法是从映射文档中提取表和列名,使用JDBC metadata API来核对每个表和列是否存在于数据库schema。这种测试执行起来很快速,因为它仅执行很少量的数据库调用。缺点就是你必须编写一些代码执行这种测试。
一个非常容易的选择是使用一些ORM框架(比如Hibernate)的schema生成特性。这些ORM框架提供一个生成SQL脚本的API,这个脚本用于增加缺少的表和字段的。这确实是易于编写的:一个生成脚本的测试,如果生成的脚本中包含增加表或者列的SQL命令,则测试失败。
使用in-memory数据库
加速数据库级别测试的好方法是使用一个in-memory(将数据存储于内存的)SQL数据库,比如HSQLDB。in-memory数据库运行在应用的JVM中,因为没有网络交换或磁盘访问,所以比一般数据库速度快不少。因为ORM框架将应用代码与数据库层面隔离开来,所以使用in-memory数据库的某些方面是非常直接的。为了配置使用in-memory数据库的ORM框架,你通常需要提供一个适合的JDBC driver和其它的设置。当你完成这些工作后,ORM框架将自动生成恰当的SQL语句。
使用in-memory数据库的一个挑战是要确保它的schema和生产数据库的schema一致。如果ORM框架生成数据库schema的话,这并不是问题。但是,如果生产数据库schema是被分开维护的,那么它的定义可能与in-memory数据库不兼容。为了使用in-memory数据库,你将需要使用不同的schema定义或者从ORM生成它的schema。话说回来,这并不能保证in-memory数据与生产数据具有同一个schema。顺其自然的结论就是,一个in-memory数据库只适用于某些种类的测试。比如你所能做的——使用in-memory数据库测试查询。
使用in-memory数据库的另一个问题是尽管它比一般的数据库速度快,但测试仍然比那些简单对象测试(simple object tests)要慢。这是由于调用ORM框架和访问数据库引起了一些负载造成的。另外,初始化数据库使其在测试启动时达到正确状态和在测试结束时校对它的状态都使测试更加复杂化了。
4.5.3 不依赖数据库进行测试
依赖数据库的测试固然很重要,但一些不依赖数据库的测试也是值得一提的。我们校对O/R映射是否正确地映射了类和数据表字段、甚至没有数据库连接的数据列。我们也能测试使用假对象(mock objects)测试数据仓库。
校对映射文档
JDO和Hibernate使用XML文档来定义O/R映射。我们可以编写测试来校对映射文档是否正确类和数据表字段、数据表关系、数据列、外键。例如,编写下面一个测试是很容易的:校对一个类是否正确映射到一个特定数据表、类映射到数据表列的字段是否正确。
这种测试是完全有用的,因为当你忘记映射一个新定义字段时测试将失败。付出一点努力,我们也能编写校对每个类字段是否正确映射到数据表列的测试。
一个实现这种想法的直接方法是使用XMLUnit——XMLUnit是用于测试XML文档的JUnit扩展。针对ORM的测试能使用XmlUnit对文档的内容作出断言(assertions)。
例如下面,一个校对PendingOrder类是否正确映射到PENDING_ORDER表的测试:
class PendingOrderMappingTests extends XMLTestCase {
public void testMapping() throws Exception {
Document mappingDocument = ...;
assertXpathEvaluatesTo("PENDING_ORDER",
"hibernate-mapping/class[@name='PendingOrder']/@table",
mappingDocument);
...
}
}
这个测试实例扩展了XmlUnit提供的XMLTestCase类。它调用assertXpathEvaluatesTo()方法,这个方法在提供的XPath表达式不等于期待值时会跑出异常。被用在这个测试中的XPath表达式获得了
你可以XmlUnit测试ORM框架的O/R映射。而这种做法的缺点:编写XPath表达式是份棘手的工作。更好的选择是从ORM框架获得O/R映射元数据(metadata)。一些ORM框架提供了可返回用于描述映射的Java对象的API。这样,ORM测试可以利用这些对象作断言。这不需要对映射文档结构的详细了解,只用ORM框架暴露必要的API。我将在第5、6章中详细说明如何编写ORM测试。
使用假对象(Mock Object)测试数据仓库
我们能使用数据库级别的测试来测试一个数据仓库。例如,一个测试数据仓库方法(repository method)的方式:执行查询来装载测试对象的数据,调用数据仓库方法,校对查询返回结果是否为期待的对象。这个方式的问题在于同时对一些事情进行测试:数据仓库,执行的查询,O/R映射。这种测试需要一些设置,并且执行缓慢。
更佳的方式为减少访问数据库次数和测试实例数量,这便是我要说明的方式——使用假对象测试数据仓库、依赖数据库测试查询。
例如,PendingOrder.createPendingOrder()方法会在数据库中建立PendingOrder对象。一种测试方法是:编写测试调用该方法(createPendingOrder()方法),然后校对它在数据库中插入的那个新行是否为PendingOrder对象的数据。但是,如果你已经编写了对O/R映射的测试,那么你可以假设HibernateTemplate.save()或JdoTemplate.makePersistent()可以正常地工作。于是这个测试在数据仓库调用save()或者makePersistent()方法时无需测试PendingOrder对象是否被插入到PENDING_ORDER表中。
因此,我们可以简单地对HibernateTemplate或者JdoTemplate使用假对象来加速数据仓库的测试。
4.5.4 ORMUnit概况
为了易于编写O/R映射和持久化对象,我已经编写了简单的JUnit扩展:ORMUnit。它扩展了JUnitTestCase提供一些基本类:
1.HibernateMappingTests:测试Hibernate O/R映射;
2.JDOMappingTests:测试JDO O/R映射;
3.HibernatePersistenceTests:测试Hibernate对象和查询;
4.JDOPersistenceTests:测试JDO对象和查询。
HibernateMappingTests和JDOMappingTests类简化了对O/R映射的测试。它们提供了关于校对O/R映射和数据库schema的断言。例如,使用它们可以驾轻就熟地测试某个类的所有字段(fields)是非正确映射到数据库。
JDOPersistenceTests和HibernatePersistenceTests类使对持久化对象和查询的测试成为易事。它们谨慎地开关PersistenceManager和Session;建立HibernateTemplate和JdoTemplate;提供管理事务的方法。在第5、6章你将看到这些类的测试实例。
自动化测试对于确保应用工作正常来讲可谓重要的工具。它是我们常规必需做的(就如常用牙线清洁牙齿一般)。但是当我们正在开发应用时,我们还要重视性能。
关于作者
Chris Richardson (chris@chrisrichardson.net):具有20年以上开发和构架经验。他作为《POJOs in Action》一书的作者,在书中详尽描述了如何利用POJO建立enterprise Java applications和轻型框架。Chris经营着一家旨为帮助其它软件公司快速开发更好软件的咨询公司。同时他还在Insignia、BEA和其它公司担任TL(technical leader)。Chris毕业于英格兰Cambridge大学计算机科学专业,现生活在加拿大Oakland。
Chris的WebSite和Blog:www.chrisrichardson.net
原文链接
Testing a persistent domain model Java, java, J2SE, j2se, J2EE, j2ee, J2ME, j2me, ejb, ejb3, JBOSS, jboss, spring, hibernate, jdo, struts, webwork, ajax, AJAX, mysql, MySQL, Oracle, Weblogic, Websphere, scjp, scjd
译编者序:
有时编写测试犹如漫漫人生旅途一般,我们常常面对某些抉择,对于如何选择下一步我们要做的,往往决定于我们的视野、价值观。本人翻译、编写此文旨在于为大家提供测试过程中一些参考
标签: