关键字:Tapestry4 单元测试 EasyMock SpringMock
1 序
在Tapestry4中进行单元测试不是一件容易的事情。我们知道,一个完整的MVC框架内的单元测试,仅仅像纯粹简单java类那样测试是不够的。除了需要模拟出被测试类所依赖的类或接口实现以外,还要模拟必须的Servlet组件对象,以及Session等,较为繁琐。此外,由于Tapestry4采用了抽象类和抽象方法来实现页面,又给它的单元测试带来了不便。更糟糕的是,Tapestry4自身并未提供完整的单元测试解决方案,同时又欠缺第三方测试框架的支持,因此在互联网上有人抨击Tapestry4,“它的单元测试简直不可能”。
其实,如果我们对Tapestry的框架内部结构足够熟悉,如果对Servlet容器的内部机制足够了解,实现在Tapestry4上的单元测试不会太难,但是太多条件对于大多数Tapestry Programmer尚不具备,依我们目前对Tapestry4框架结构的了解程度,仍然无法实现像Struts和Tapestry5那样完整的单元测试,但应该基本能够满足我们的测试需要。当然,随着对框架研究的逐步深入,这一情况应该会逐步得到改善。
接下来,本文将针对Tapestry4 + Hibernate + Spring框架组合中,Tapestry4的单元测试做简要说明。
2 工具介绍
2.1 EasyMock简介
EasyMock是一个Mock对象的类库。最新的EasyMock版本是2.2,可在http://sourceforge.net/projects/easymock下载到。Mock 对象能够模拟领域对象的部分行为,并且能够检验运行结果是否和预期的一致。领域类将通过与Mock 对象的交互,来获得一个独立的测试环境。EasyMock可以动态地生成Mock 对象而不需要编写它们,因为也不会产生多余代码。说明:默认情况下,EasyMock只支持为接口生成Mock。如果还需要为类生成Mock,可在上述网址下载EasyMock的扩展包以完成此功能。EasyMock 2.2只能在Java 5.0以上的版本中运行。
3 单元测试方法
3.1 方法一:EasyMock辅助方式
大多数情况下,前端的单元测试需要与后端分离。而且为了测试方便,又会希望摆脱对Servlet容器的依赖。在这种情况下,以往的实现手段是手工编写所需的Mock对象来辅助完成测试,但这样会大幅增加测试代码编写量。幸运的是,这部分工作现在可以通过EasyMock来帮助我们完成,从而提高单元测试效率。
这种测试方式下,使用JUnit或TestNG均可。
下面,以Metaone前端的登陆页面类(Login.java,为了增加例子可读性,代码已删减)的单元测试为例,讲解EasyMock辅助Tapestry4单元测试的方法。
被测试类Login.java的被测方法代码如下:
//Login.java
package com.carnation.metaone.page;
import ……
public abstract class Login extends BasePage implements PageBeginRenderListener {
// 用户名
public abstract String getUsername();
// 密码
public abstract String getPassword();
public abstract UserService getUserService();
public abstract HttpServletRequest getHttpServletRequest();
public abstract User getUser();
public abstract void setUser(User user);
public abstract void setApplication(Application app);
public abstract Application getApplication();
// 登陆表单的监听方法
public IPage loginSubmit(IRequestCycle cycle) {
User user = new User();
user.setUsername(getUsername());
user.setPassword(getPassword());
Application application = getApplication();
user.setApplication(application);
try {
user = getUserService().authenticate(user);
user.setLocale(this.getLocale());
user.setApplication(application);
setUser(user);
Date myDate = new Date();
String ip = getHttpServletRequest().getRemoteAddr().trim();
/*
* 跳转页面,其中空URL代表当前应用的根目录
*/
String url = "Home.html";
if (application.getId().intValue()>1) {
url = "application/Home.html";
}
return cycle.getPage(url);
} catch (AuthenticationException e) {
IValidationDelegate delegate = getDelegate();
delegate.setFormComponent(null);
delegate.record("认证失败,请重新登录!", null);
return null;
}
}
测试代码编写步骤:
(1) 把EasyMock包引入测试类,当然如果使用JDK1.5,可以使用静态引入,使用更加方便。
import static org.easymock.EasyMock; //JDK1.5环境下可以静态引入
import static org.easymock.EasyMock.createMock;
import static org.easymock.EasyMock.expect;
import static org.easymock.EasyMock.replay;
import static org.easymock.EasyMock.verify;
(2) 模拟后端Service接口和Servlet接口。
UserService userService = createMock(UserService.class);
HttpServletRequest httpServletRequest = create(HttpServletRequest);
IRequestCycle cycle = create(IRequestCycle.class);
Home home = create(Home.class);
(3) 将Login类实例化,并将需要初始化的变量进行初始化。Tapestry框架提供了一个org.apache.tapestry.test.Creator类,可以创建页面和组件抽象类的实例。
Creator creator = new Creator();Map propertyMap = new HashMap();propertyMap.add(“username”,”admin”);
propertyMap.add(“password”,”admin”);
propertyMap.add(“userService”,userService);
Application application = new Application();
application.setId(new Long(“1”));
propertyMap.add(“application”,application);creator.newInstance(Login.class, propertyMap);
(4) 编写Mock行为逻辑。EasyMock测试注重行为,即通过类似于录制、回放的过程来进行测试。在“录制”过程中,我们编写期望Mock对象发生的行为,例如返回值、被调用次数等。在回放过程中,EasyMock将自动检查实际发生行为与预期是否相符。那么,对应本例而言,代码如下:
expect(httpServletReques.getRemoteAddr()).andReturn(“192.168.0.1”);
try{
expect(userService.authenticate(not(eq(user)))).andReturn(getMockUser());
}catch(AuthenticationException e){ ……}
expect(cycle.getPage(“application/Home.html”)).andReturn(home).anyTimes();
expect(cycle.getPage(“Home.html”)).andReturn(home).anyTimes();
replay(httpServletReques);replay(userService);replay(cycle);
Creator creator = new Creator();
Login loginPage = (Login) creator.newInstance(Login.class, propertyMap);
(5) 断言
assert loginPage.loginSubmit(cycle).equals(home);
3.2 方法二:SpringMock辅助方式(前后端集成)
在一些实际情况中,后端的一些方法内部对Hibernate Session进行操作,在这种情况下,对于那些无返回值的方法调用,在前端使用EasyMock是无法模拟的。
幸运的是,利用Spring Mock可以辅助这类测试。但需要注意,这并不意味这不再需要EasyMock, Servlet接口的模拟仍然需要使用EasyMock来实现。
若采用这种方式进行测试,对测试工具的选择会有局限,仅支持JUnit,因为Spring Mock提供的基类继承自JUnit中的测试类。
该方式的实现步骤如下:
(1) 使测试类继承AbstractDependencyInjectionSpringContextTests。
(2) 覆写getConfigLocations方法。以读取Spring上下文。
@Override
protected String[] getConfigLocations() {String[] str = new String[]{"applicationContext.xml"};
return str;
}
(3) 分别在onSetUp()和onTearUp()方法中开启和关闭Hibernate的Session,这样延迟加载(Lazy Initialization)的集合也可以被正常调用。
private SessionFactory sf;@Override
protected void onSetUp() throws Exception {
super.onSetUp();
SessionFactory sessionFactory = (SessionFactory) applicationContext.getBean("sessionFactory");
sf = sessionFactory;Session s = sessionFactory.openSession();
TransactionSynchronizationManager.bindResource(sessionFactory, new SessionHolder(s));
}
@Override
protected void onTearDown() throws Exception {
super.onTearDown();
SessionHolder holder = (SessionHolder)TransactionSynchronizationManager.getResource(sf);
Session s = holder.getSession();
s.flush();TransactionSynchronizationManager.unbindResource(sf);
SessionFactoryUtils.closeSessionIfNecessary(s, sf);}
(4) 编写测试方法。与使用EasyMock不同,此时通过application对象的getBean()方法来获取Spring上下文中装配的Bean。例如
UserService userService = (UserService)applicationContext.getBean("userService"); 通过上述两种方法,可以满足大部分单元测试需要,但仍然无法覆盖到界面操作方面的测试。这样的做法在理论上是可行的,但从实际角度出发,对于Web层质量的提升意义并不大,不如用Selenium做集成测试来的实在。:)
发表评论
| 姓名: | |
| E-mail: | |
| 地址: | |


评论
我尝试用HTTPunit..很遗憾。失败了。我感觉T的测试实在难做了,
Tapestry4的单元测试的确很难做,需要模拟的东西太多,而框架本身却没有给予支持。T5我没有用过,但从我对它零零散散的了解来看,T5在单元测试方面确实下了功夫,有机会应该试一试。HTTPUnit用来做集成测试可以用一下,但功能没有Selenimu强大。
(2007-12-06 00:35:11)