频道直达 - 学院 - 下载 - 交易 - 特效 - 字库 - 手册 -排名-工具- 繁體
网页教学网站开发 设为首页
加入收藏
联系我们
建站搜索: 常用广告代码   用户注册 | 用户登陆
您当前的位置:中国建站之家 -> 网站开发设计技术教程 -> JSP教程 -> 对J2EE中的DAO组件编写单元测试

对J2EE中的DAO组件编写单元测试

作者:未知  来源:www.jz123.cn  发布时间:2007-8-23 10:05:39  发布人:圈圈

减小字体 增大字体

单元测试作为保证软件质量及重构的基础,早已获得广大开发人员的认可。单元测试是一种细粒度的测试,越来越多的开发人员在提交功能模块时也同时提交相应的单元测试。对于大多数开发人员来讲,编写单元测试已经成为开发过程中必须的流程和最佳实践。

  对普通的逻辑组件编写单元测试是一件容易的事情,由于逻辑组件通常只需要内存资源,因此,设置好输入输出即可编写有效的单元测试。对于稍微复杂一点的组件,例如Servlet,我们可以自行编写模拟对象,以便模拟HttpRequest和HttpResponse等对象,或者,使用EasyMock之类的动态模拟库,可以对任意接口实现相应的模拟对象,从而对依赖接口的组件进行有效的单元测试。

  在J2EE开发中,对DAO组件编写单元测试往往是一件非常复杂的任务。和其他组件不通,DAO组件通常依赖于底层数据库,以及JDBC接口或者某个ORM框架(如Hibernate),对DAO组件的测试往往还需引入事务,这更增加了编写单元测试的复杂性。虽然使用EasyMock也可以模拟出任意的JDBC接口对象,或者ORM框架的主要接口,但其复杂性往往非常高,需要编写大量的模拟代码,且代码复用度很低,甚至不如直接在真实的数据库环境下测试。不过,使用真实数据库环境也有一个明显的弊端,我们需要准备数据库环境,准备初始数据,并且每次运行单元测试后,其数据库现有的数据将直接影响到下一次测试,难以实现“即时运行,反复运行”单元测试的良好实践。

  本文针对DAO组件给出一种较为合适的单元测试的编写策略。在JavaEE开发网的开发过程中,为了对DAO组件进行有效的单元测试,我们采用HSQLDB这一小巧的纯Java数据库作为测试时期的数据库环境,配合Ant,实现了自动生成数据库脚本,测试前自动初始化数据库,极大地简化了DAO组件的单元测试的编写。
在Java领域,JUnit作为第一个单元测试框架已经获得了最广泛的应用,无可争议地成为Java领域单元测试的标准框架。本文以最新的JUnit 4版本为例,演示如何创建对DAO组件的单元测试用例。

  JavaEEdev的持久层使用Hibernate 3.2,底层数据库为MySQL。为了演示如何对DAO进行单元测试,我们将其简化为一个DAOTest工程:

  由于将Hibernate的Transaction绑定在Thread上,因此,HibernateUtil类负责初始化SessionFactory以及获取当前的Session:

 
 public class HibernateUtil {
  private static final  SessionFactory sessionFactory;
  static {
  try {
  sessionFactory =  new AnnotationConfiguration()
  .configure()
  .buildSessionFactory();
  }
  catch(Exception e) {
  throw new  ExceptionInInitializerError(e);
  }
  }
  public static Session  getCurrentSession() {
  return  sessionFactory.getCurrentSession();
  }
  }

  HibernateUtil还包含了一些辅助方法,如:

 public static Object query(Class clazz, Serializable id);
  public static void createEntity(Object entity);
  public static Object queryForObject(String hql, Object[] params);
  public static List queryForList(String hql, Object[] params);
    在此不再多述。

  实体类User使用JPA注解,代表一个用户:

  @Entity
  @Table(name="T_USER")
  public class User {
  public static final  String REGEX_USERNAME = "[a-z0-9][a-z0-9\\-]{1,18}[a-z0-9]";
  public static final  String REGEX_PASSWORD = "[a-f0-9]{32}";
  public static final  String REGEX_EMAIL = "([0-9a-zA-Z]([-.\\w]*[0-9a-zA-Z])*@([0-9a-zA-Z][-\\w]*[0-9a-zA-Z]\\.)+[a-zA-Z]{2,9})"; 
  private String  username;     // 用户名
  private String  password;     // MD5口令
  private boolean  admin;       // 是否是管理员
  private String  email;        // 电子邮件
  private int  emailValidation; // 电子邮件验证码
  private long  createdDate;    // 创建时间
  private long  lockDate;       // 锁定时间  
  public User() {} 
  public User(String  username, String password, boolean admin, long lastSignOnDate) {
  this.username = username;
  this.password =  password;
  this.admin = admin;
  } 
  @Id
  @Column(updatable=false,  length=20)
  @Pattern(regex=REGEX_USERNAME)
  public String  getUsername() { return username; }
  public void setUsername(String  username) { this.username = username; } 
  @Column(nullable=false,  length=32)
  @Pattern(regex=REGEX_PASSWORD)
  public String  getPassword() { return password; }
  public void  setPassword(String password) { this.password = password; } 
  @Column(nullable=false,  length=50)
  @Pattern(regex=REGEX_EMAIL)
  public String getEmail()  { return email; }
  public void  setEmail(String email) { this.email = email; } 
  @Column(nullable=false)
  public boolean getAdmin()  { return admin; }
  public void  setAdmin(boolean admin) { this.admin = admin; } 
  @Column(nullable=false,  updatable=false)
  public long  getCreatedDate() { return createdDate; }
  public void  setCreatedDate(long createdDate) { this.createdDate = createdDate; } 
  @Column(nullable=false)
  public int  getEmailValidation() { return emailValidation; }
  public void  setEmailValidation(int emailValidation) { this.emailValidation =  emailValidation; } 
  @Column(nullable=false)
  public long getLockDate()  { return lockDate; }
  public void  setLockDate(long lockDate) { this.lockDate = lockDate; }
  @Transient
  public boolean  getEmailValidated() { return emailValidation==0; } 
  @Transient
  public boolean  getLocked() {
  return !admin  && lockDate>0 && lockDate>System.currentTimeMillis();
  }
  }

    实体类PasswordTicket代表一个重置口令的请求:

  @Entity
  @Table(name="T_PWDT")
  public class PasswordTicket {
  private String id;
  private User user;
  private String ticket;
  private long createdDate;
  @Id
  @Column(nullable=false, updatable=false,  length=32)
  @GeneratedValue(generator="system-uuid")
  @GenericGenerator(name="system-uuid",  strategy="uuid")
  public String getId() {  return id; }
  protected void  setId(String id) { this.id = id; }
  @ManyToOne
  @JoinColumn(nullable=false, updatable=false)
  public User getUser() {  return user; }
  public void setUser(User  user) { this.user = user; }
  @Column(nullable=false,  updatable=false, length=32)
  public String getTicket()  { return ticket; }
  public void  setTicket(String ticket) { this.ticket = ticket; }
  @Column(nullable=false,  updatable=false)
  public long  getCreatedDate() { return createdDate; }
  public void  setCreatedDate(long createdDate) { this.createdDate = createdDate; }
  }

    UserDao接口定义了对用户的相关操作:

 public interface UserDao {
  User  queryForSignOn(String username);
  User queryUser(String  username);
  void createUser(User  user);
  void updateUser(User  user);
  boolean  updateEmailValidation(String username, int ticket);
  String  createPasswordTicket(User user);
  boolean  updatePassword(String username, String oldPassword, String newPassword);
  boolean  queryResetPassword(User user, String ticket);
  boolean  updateResetPassword(User user, String ticket, String password);
  void updateLock(User  user, long lockTime);
  void updateUnlock(User  user);
  }
    UserDaoImpl是其实现类:

 public class UserDaoImpl implements UserDao {
  public User  queryForSignOn(String username) {
  User user =  queryUser(username);
  if(user.getLocked())
  throw new  LockException(user.getLockDate());
  return user;
  }
 public User  queryUser(String username) {
  return (User)  HibernateUtil.query(User.class, username);
  }
 public void  createUser(User user) {
  user.setEmailValidation((int)(Math.random() * 1000000) + 0xf);
  HibernateUtil.createEntity(user);
  }
  // 其余方法略
  ...
  }
    由于将Hibernate事务绑定在Thread上,因此,实际的客户端调用DAO组件时,还必须加入事务代码:

 Transaction tx =  HibernateUtil.getCurrentSession().beginTransaction();
  try {
  dao.xxx();
  tx.commit();
  }
  catch(Exception e) {
  tx.rollback();
  throw e;
  }

  下面,我们开始对DAO组件编写单元测试。前面提到了HSQLDB这一小巧的纯Java数据库。HSQLDB除了提供完整的JDBC驱动以及事务支持外,HSQLDB还提供了进程外模式(与普通数据库类似)和进程内模式(In-Process),以及文件和内存两种存储模式。我们将HSQLDB设定为进程内模式及仅使用内存存储,这样,在运行JUnit测试时,可以直接在测试代码中启动HSQLDB。测试完毕后,由于测试数据并没有保存在文件上,因此,不必清理数据库。

  此外,为了执行批量测试,在每个独立的DAO单元测试运行前,我们都执行一个初始化脚本,重新建立所有的表。该初始化脚本是通过HibernateTool自动生成的,稍后我们还会讨论。下图是单元测试的执行顺序:

  在编写测试类之前,我们首先准备了一个TransactionCallback抽象类,该类通过Template模式将DAO调用代码通过事务包装起来:

 public abstract class TransactionCallback {
  public final Object  execute() throws Exception {
  Transaction tx =  HibernateUtil.getCurrentSession().beginTransaction();
  try {
  Object r =  doInTransaction();
  tx.commit();
  return r;
  }
  catch(Exception e) {
  tx.rollback();
  throw e;
  }
  }
  // 模板方法:
  protected abstract Object  doInTransaction() throws Exception;
  }
    其原理是使用JDK提供的动态代理。由于JDK的动态代理只能对接口代理,因此,要求DAO组件必须实现接口。如果只有具体的实现类,则只能考虑CGLIB之类的第三方库,在此我们不作更多讨论。

  下面我们需要编写DatabaseFixture,负责启动HSQLDB数据库,并在@Before方法中初始化数据库表。该DatabaseFixture可以在所有的DAO组件的单元测试类中复用:

 public class DatabaseFixture {
  private static Server  server = null; // 持有HSQLDB的实例
  private static final  String DATABASE_NAME = "javaeedev"; // 数据库名称
  private static final  String SCHEMA_FILE = "schema.sql"; // 数据库初始化脚本
  private static final  List<String> initSqls = new ArrayList<String>();
 @BeforeClass // 启动HSQLDB数据库
  public static void  startDatabase() throws Exception {
  if(server!=null)
  return;
  server = new  Server();
  server.setDatabaseName(0,  DATABASE_NAME);
  server.setDatabasePath(0, "mem:" + DATABASE_NAME);
  server.setSilent(true);
  server.start();
  try {
  Class.forName("org.hsqldb.jdbcDriver");
  }
  catch(ClassNotFoundException cnfe) {
  throw new  RuntimeException(cnfe);
  }
  LineNumberReader  reader = null;
  try {
  reader = new  LineNumberReader(new     InputStreamReader(DatabaseFixture.class.getClassLoader().getResourceAsStream(SCHEMA_FILE)));
  for(;;) {
  String line =  reader.readLine();
  if(line==null) break;
  // 将text类型的字段改为varchar(2000),因为HSQLDB不支持text:
  line =  line.trim().replace(" text ", " varchar(2000)  ").replace("  text,", " varchar(2000),");
  if(!line.equals(""))
  initSqls.add(line);
  }
  }
  catch(IOException e)  {
  throw new  RuntimeException(e);
  }
  finally {
  if(reader!=null)  {
  try {  reader.close(); } catch(IOException e) {}
  }
  }
  }
 @Before // 执行初始化脚本
  public void initTables()  {
  for(String sql :  initSqls) {
  executeSQL(sql);
  }
  }
static Connection  getConnection() throws SQLException {
  return  DriverManager.getConnection("jdbc:hsqldb:mem:" + DATABASE_NAME,  "sa", "");
  }
static void  close(Statement stmt) {
  if(stmt!=null) {
  try {
  stmt.close();
  }
  catch(SQLException e) {}
  }
  }
static void  close(Connection conn) {
  if(conn!=null) {
  try {
  conn.close();
  }
  catch(SQLException e) {}
  }
  }
static void  executeSQL(String sql) {
  Connection conn =  null;
  Statement stmt =  null;
  try {
  conn =  getConnection();
  boolean  autoCommit = conn.getAutoCommit();
  conn.setAutoCommit(true);
  stmt = conn.createStatement();
  stmt.execute(sql);
  conn.setAutoCommit(autoCommit);
  }
  catch(SQLException e)  {
  log.warn("Execute failed: " + sql + "\nException: "  + e.getMessage());
  }
  finally {
  close(stmt);
  close(conn);
  }
  }
public static Object  createProxy(final Object target) {
  return  Proxy.newProxyInstance(
  target.getClass().getClassLoader(),
  target.getClass().getInterfaces(),
  new  InvocationHandler() {
  public  Object invoke(Object proxy, final Method method, final Object[] args) throws  Throwable {
  return new TransactionCallback() {
  @Override
  protected Object doInTransaction() throws Exception {
  return method.invoke(target, args);
  }
  }.execute();
  }
  }
  );
  }
  }
    注意DatabaseFixture的createProxy()方法,它将一个普通的DAO对象包装为在事务范围内执行的代理对象,即对于一个普通的DAO对象的方法调用前后,自动地开启事务并根据异常情况提交或回滚事务。

  下面是UserDaoImpl的单元测试类:

 public class UserDaoImplTest extends DatabaseFixture {
  private UserDao userDao =  new UserDaoImpl();
  private UserDao proxy =  (UserDao)createProxy(userDao);
 @Test
  public void  testQueryUser() {
  User user =  newUser("test");
  proxy.createUser(user);
  User t =  proxy.queryUser("test");
  assertEquals(user.getEmail(), t.getEmail());
  }
  }
    注意到UserDaoImplTest持有两个UserDao引用,userDao是普通的UserDaoImpl对象,而proxy则是将userDao进行了事务封装的对象。

  由于UserDaoImplTest从DatabaseFixture继承,因此,@Before方法在每个@Test方法调用前自动调用,这样,每个@Test方法执行前,数据库都是一个经过初始化的“干净”的表。

  对于普通的测试,如UserDao.queryUser()方法,直接调用proxy.queryUser()即可在事务内执行查询,获得返回结果。

  对于异常测试,例如期待一个ResourceNotFoundException,就不能直接调用proxy.queryUser()方法,否则,将得到一个UndeclaredThrowableException:

  这是因为通过反射调用抛出的异常被代理类包装为UndeclaredThrowableException,因此,对于异常测试,只能使用原始的userDao对象配合TransactionCallback实现:

 @Test(expected=ResourceNotFoundException.class)
  public void testQueryNonExistUser() throws Exception {
  new TransactionCallback()  {
  protected Object  doInTransaction() throws Exception {
  userDao.queryUser("nonexist");
  return null;
  }
  }.execute();
  }

    到此为止,对DAO组件的单元测试已经实现完毕。下一步,我们需要使用HibernateTool自动生成数据库脚本,免去维护SQL语句的麻烦。相关的Ant脚本片段如下:

 <target name="make-schema" depends="build"  description="create schema">
  <taskdef  name="hibernatetool" classname="org.hibernate.tool.ant.HibernateToolTask">
  <classpath  refid="build-classpath"/>
  </taskdef>
  <taskdef  name="annotationconfiguration"  classname="org.hibernate.tool.ant.AnnotationConfigurationTask">
  <classpath  refid="build-classpath"/>
  </taskdef>
  <annotationconfiguration  configurationfile="${src.dir}/hibernate.cfg.xml"/>
  <hibernatetool  destdir="${gen.dir}">
  <classpath  refid="build-classpath"/>
  <annotationconfiguration  configurationfile="${src.dir}/hibernate.cfg.xml"/>
  <hbm2ddl
  export="false"
  drop="true"
  create="true"
  delimiter=";"
  outputfilename="schema.sql"
  destdir="${src.dir}"
  />
  </hibernatetool>
  </target>

    完整的Ant脚本以及Hibernate配置文件请参考项目工程源代码。

  利用HSQLDB,我们已经成功地简化了对DAO组件进行单元测试。我发现这种方式能够找出许多常见的bug:

HQL语句的语法错误,包括SQL关键字和实体类属性的错误拼写,反复运行单元测试就可以不断地修复许多这类错误,而不需要等到通过Web页面请求而调用DAO时才发现问题;传入了不一致或者顺序错误的HQL参数数组,导致Hibernate在运行期报错;

一些逻辑错误,包括不允许的null属性(常常由于忘记设置实体类的属性),更新实体时引发的数据逻辑状态不一致。

  总之,单元测试需要根据被测试类的实际情况,编写最简单最有效的测试用例。本文旨在给出一种编写DAO组件单元测试的有效方法。


将本文收藏到QQ书签与更多好友分享
[打 印]
[] [返回上一页] [收 藏]
∷相关文章评论∷    (评论内容只代表网友观点,与本站立场无关!) [更多评论...]
精彩推荐
热门文章
· 注册码大全二
· 注册码大全四
· 注册码大全一
· 要10G免费网络硬盘的请进..
· 通过google 赶快来赚美金..
· 注册码大全十
· 头像-qq头像(qq新头像)4..
· 让你轻松架设FTP服务器1..
· 注册码大全三
· 梦幻背景图片7
· 卡通动物图片6
· 网页制作素材-按钮素材2..
· 让你轻松架设FTP服务器5..
· 风景图片8
· 注册码大全九
· 让你轻松架设FTP服务器2..
关注此文读者还看过
· 任何浏览器都可以弹广告..
· 用 Photoshop 合成的机械..
· 思考:不要好高务远 做有..
· asp之日期和时间函数示例..
· Photoshop CS3绘制可爱卡..
· 去除DW MX 2004表格宽度..
· 摩根士丹利两份报告助推..
· 在ASP中常见的错误80004..
· 正确配置ODBC连接sybase..
· 在ASP中列出数据库中的表..
· 利用OWC画图的例子
· JScript 方法 - isNaN 方..
· 词语搭配游戏的制作(AS..
· .NET让新一代因特网变成..
· Fireworks 仿MSN浏览器按..
· 个人网站真能转成商业网..
相关文章
关于本站 - 网站帮助 - 广告合作 - 下载声明 - 友情连接 - 网站地图 - 人才招聘
网站合作、内容监督、商务咨询:QQ: 9576619
Copyright ? 2005--2008 中国建站之家版权所有
粤ICP备05092265号