| 首 页 | AJAX请求并发问题  >>
2007-05-24

关键字:Tapestry4  单元测试  EasyMock  SpringMock

1        

Tapestry4中进行单元测试不是一件容易的事情。我们知道,一个完整的MVC框架内的单元测试,仅仅像纯粹简单java类那样测试是不够的。除了需要模拟出被测试类所依赖的类或接口实现以外,还要模拟必须的Servlet组件对象,以及Session等,较为繁琐。此外,由于Tapestry4采用了抽象类和抽象方法来实现页面,又给它的单元测试带来了不便。更糟糕的是,Tapestry4自身并未提供完整的单元测试解决方案,同时又欠缺第三方测试框架的支持,因此在互联网上有人抨击Tapestry4,“它的单元测试简直不可能”。

其实,如果我们对Tapestry的框架内部结构足够熟悉,如果对Servlet容器的内部机制足够了解,实现在Tapestry4上的单元测试不会太难,但是太多条件对于大多数Tapestry Programmer尚不具备,依我们目前对Tapestry4框架结构的了解程度,仍然无法实现像StrutsTapestry5那样完整的单元测试,但应该基本能够满足我们的测试需要。当然,随着对框架研究的逐步深入,这一情况应该会逐步得到改善。

接下来,本文将针对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来帮助我们完成,从而提高单元测试效率。

这种测试方式下,使用JUnitTestNG均可。

下面,以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()方法中开启和关闭HibernateSession,这样延迟加载(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做集成测试来的实在。:)






评论

  • 我尝试用HTTPunit..很遗憾。失败了。我感觉T的测试实在难做了,

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

    alxyrh () 发表于 2007-12-04 15:57:55  [回复]

发表评论

 姓名:
 E-mail:
 地址: