使用模拟和实际数据访问对象来有效而又高效地测试 J2EE 应用程序
数据访问对象(Data Access Object)模式已经成为 J2EE 开发人员工具库中的标准部件。大多数开发人员不知道它有一个变体可以使测试更轻松。 模拟数据访问对象集中了 DAO、模仿对象和分层测试的精华,从而允许您同时改进测试结果和整体开发方法。企业 Java 开发人员(并且是 SDAO 大师)Kyle Brown 使用代码样本和讨论向您全面介绍 SDAO 的概念和日常用法。
今年早些时候,我在北卡罗莱纳州立大学(North Carolina State University)给一群研究生作了一个关于 J2EE 技术的演讲。我提供了一个说明 servlet 和 JSP 用法的样本设计,以说明如何结合 MVC 方法和简单数据访问对象(DAO)以处理应用程序中持久数据的查询和更新。在陈述这些材料时,我看到听众们会心地微笑和点头。但是,在陈述结束之后,我收到了一个学生的电子邮件。为什么我实现的 DAO 不止一个,而是两个,又为什么设置了一个 Factory 类以允许在它们之间进行选择呢?
这很令人费解,但是我很快意识到了问题所在:我忘了告诉那些学生,我正在实现 MVC 和 DAO 之外的第三种模式。尤其是,我的设计利用了模仿对象来简化测试。在我考虑 DAO 的方式中,模仿对象是很基本的,以致我无法想象在缺少其中一个的情况下如何实现另一个。进一步说,我自动地调整了设计,以便使测试更容易,这对许多经验丰富的开发人员和我的学生们都是一个同样陌生的概念。
在本文中,您将了解使用模拟数据访问对象(SDAO)进行分层测试。与模仿对象一样,SDAO 用专门为测试开发的对象取代现有对象。但是,与模仿对象不同的是,不需要对 SDAO 提供检测以包含测试断言。SDAO 模式的目的是允许您将一个层(域对象层)和另一个层(数据访问层)隔离开。我们将从回顾数据访问对象模式入手。
数据访问对象 数据访问对象模式的目的是提供到特定数据源的单个联系点。与许多设计模式相似,DAO 以分隔设计任务为基础。具体来说,DAO 将业务逻辑与数据库持久性代码分隔开来。数据访问对象负责对持有数据(存储在关系数据库中)的对象进行操作。DAO 所操作的对象通常称为 值对象,尽管术语 数据传送对象(data transfer object,DTO)的意思更为贴切。
DAO 类通常包含对其 DTO 进行操作的 CRUD 方法,即 create() 、 read() 、 update() 和 delete() 。例如,假定我们正在构建一个登记会议出席人员的系统。我们的 DAO 类接口可能如清单 1 中所示:
清单 1. 示例 DAO 类接口
public interface AttendeeDAO {
public void createAttendee(Attendee person) throws DAOException;
public void updateAttendee(Attendee person) throws DAOException;
public Collection getAllAttendees() throws DAOException;
public void deleteAttendee(Attendee person) throws DAOException;
public Attendee findAttendeeForPrimaryKey(int primaryKey)
throws DAOException;
|
声明这个接口(或与它类似的接口)是实现 DAO 模式的标准部分。具体的 DAO 应用程序将实现如图 1 中所示的接口:
图 1. 示例 DAO 应用程序
消息处理 在大多数情况下,类似于上述示例的 DAO 应用程序会使用 JDBC 进行数据库访问。例如,在 getAllAttendees() 方法的示例中,该类将获取一个到数据库的连接,查询数据库以获取 Attendee 表中的行列表,迭代从查询返回的 ResultSet ,然后为 ResultSet 中每一行构造 Attendee 对象。
请参阅 参考资料以获取 DAO 模式的深入讨论和示例。
模拟数据访问对象 模拟数据访问对象实际上模拟后端数据存储。实现 SDAO 模式使我们能够测试各种应用程序层(如业务逻辑和 GUI),而无需恰好拥有实际的数据库。
以下是使用 SDAO 进行分层测试的一些具体优点:
- 便宜:使用模拟数据库进行测试和调试使您节省了在每个开发人员的桌面上安装 DB2(比方说)的成本。
- 容易:即使不必处理数据库错误,构建具有 servlet、JSP 文件和 EJB 组件的应用程序也够复杂的了。分层测试允许您设计出表示和业务逻辑,而不必同时担心后端数据库。
- 迅速:按层进行分隔允许您在出现问题时隔离它们,这使得调试周期更快。有些错误(如
TransactionRollbackException )难以定位;从综合体中除去数据库层让您更快地找出真实的问题。
- 灵活:SDAO 可以用于性能概要分析和测试。尽管有些类型的性能问题(如数据库死锁)需要实际的数据库来解决,但 SDAO 让您获得仅域和 GUI 性能的测量结果,然后可以利用这些结果来解决那些层上的问题。
SDAO 实战 理解 SDAO 如何工作的最佳方法是实际研究它,并希望您亲自应用它。我们将从模拟数据访问对象的简单示例入手,如清单 2 所示:
清单 2. DefaultDAO
public class DefaultDAO implements AttendeeDAO {
private static DefaultDAO instance = new DefaultDAO();
private Vector attendees = new Vector();
/**
* @see AttendeeDAO#createAttendee(Attendee)
*/
public void createAttendee(Attendee person) throws DAOException {
getAllAttendees().add(person);
}
/**
* @see AttendeeDAO#updateAttendee(Attendee)
*/
public void updateAttendee(Attendee person) throws DAOException {
Attendee match =
findAttendeeForPrimaryKey(person.getAttendeeKey());
attendees.remove(match);
attendees.add(person);
}
/**
* @see AttendeeDAO#getAllAttendees()
*/
public Collection getAllAttendees() throws DAOException {
return getAttendees();
}
/**
* @see AttendeeDAO#deleteAttendee(Attendee)
*/
public void deleteAttendee(Attendee person) throws DAOException {
Attendee match =
findAttendeeForPrimaryKey(person.getAttendeeKey());
attendees.remove(match);
attendees.add(person);
}
/**
* Gets the attendees
* @return Returns a Vector
*/
public Vector getAttendees() {
return attendees;
}
/**
* Sets the attendees
* @param attendees The attendees to set
*/
public void setAttendees(Vector attendees) {
this.attendees = attendees;
}
/**
* Gets the instance
* @return Returns a DefaultDAO
*/
public static DefaultDAO getInstance() {
return instance;
}
/**
* Sets the instance
* @param instance The instance to set
*/
public static void setInstance(DefaultDAO anInstance) {
instance = anInstance;
}
public Attendee findAttendeeForPrimaryKey(int primaryKey)
throws DAOException {
Enumeration enum = attendees.elements();
while (enum.hasMoreElements()) {
Attendee current = (Attendee) enum.nextElement();
if (current.getAttendeeKey() == primaryKey)
return current;
}
throw new DAOException("Primary Key not found "
+ primaryKey);
}
}
|
现在来看一下,这是晦涩的火箭科学,是吗? DefaultDAO 类在静态变量 instance 中存储了它自己的一个实例(存储为 Singleton),并允许通过 getInstance() 方法访问该实例。然后,该类的用户可以在 Singleton 实例保存的集合中添加和删除 Attendee 元素,或替换集合中的元素。
对象工厂 要使这种技术在实际工作中有效,需要能够将程序中的“实际”DAO 类替换成新的“模拟”DAO 类。我们的客户机代码本身不能引用 Db2AttendeeDAO 类或 DefaultDAO 类。因此我们使用 Factory 类(又名对象工厂),以根据需要为客户机代码提供 Db2AttendeeDAO 和 DefaultDAO 实例。
我们的对象工厂相当简单。它只返回两个类实例,用一种软件“开关”(如 getAttendeeDAO() 方法中所示)在两者之间进行切换。这个开关还可以检查 System 特性的值,或检查一些其它全局值,如清单 3 所示:
清单 3. AttendeeDAOFactory
public class AttendeeDAOFactory {
public static AttendeeDAO getAttendeeDAO() {
String mode = (String) System.getProperty("TestMode");
if (mode.equals("Simulated"))
return DefaultDAO.getInstance();
else
return new DbAttendeeDAO();
}
}
|
当您运行测试时,通常首先将 Factory 开关设置成返回模拟类。这样做确保您可以在与数据库隔离的情况下测试系统的其余层。只有在后面的测试中才会将开关设置成返回“实际”DAO。图 2 使您对最终设计(包括 Factory 类)有一些了解,有可能如下所示:
图 2. 示例 SDAO 实现
高级 SDAO 在您理解了基本的 SDAO 实现之后,就可以研究其它使用模拟 DAO( DefaultDAO )类的方法。迄今为止,您所看到的只是最简单的实现,其中的结果取自内存中的集合,该集合在测试期间必须被填充。这种思想的常见扩展是在类的构造函数中预先用缺省值填充该集合。正如我们在此所做的,使用 Singleton 的主要缺点是:您必须在各次测试之间清除 Singleton。如果您在某次测试时忘了这样做,则会导致后面的测试失败。幸运的是,大多数单元测试框架(如 JUnit)提供了轻松进行此类测试的工具。例如,在 JUnit 中,可以将清除 Singleton 的代码放入测试类的 teardown() 方法中,并将任何执行预先填充工作的代码放到该测试类的 setUp() 方法中。
第二种方法略微复杂些,但也是为更实际的测试所提供的,这就是使用 Java 序列化或 XML 从文件读取一组对象。这两种技术都允许您使用几个文件来表示同一个测试的不同初始条件。
我经常在 SDAO 测试中使用“分两步走”的方法。我构建的第一个 DAO 是“缺省”DAO;它转至 DTO 内存中集合,然后被传递给构建应用程序中上层部分(例如 servlet 和 JSP 文件)的团队。然后我和另一个团队一起构建将实际使用数据库的 DAO。这种方法允许两个团队同时工作,他们的交互是由 DAO 接口的共享约定定义的。
结束语 在 IBM WebSphere 软件服务组(Software Services for WebSphere group)中,我们已经成功地将本文描述的分层测试技术应用到了数十个客户合作项目中。除了改进我们的整个产品之外,SDAO 还成为了帮助我们团队掌握各种 J2EE API 特性的重要工具。使用模拟和实际 DAO 使我们可以在应用程序的许多层上同时工作,而不会被一次性地将所有部分组装到一起的复杂情况所“吓倒”。
作者衷心地感谢 Stacy Joines 和 Ken Hygh 对本文提出的有益建议。
参考资料
|