首页 新闻 论坛 群组 Blog 文档 下载 读书 Tag 网摘 搜索 开源 FAQ 第二书店 博文视点 程序员
频道: 研发 数据库 中间件 信息化 视频 .NET Java 游戏 移动 服务: 人才 外包 培训
    图书品种:235680
       
热门搜索: ASP.NET Ajax Spring Hibernate Java

18.3.1  Web组件的单元测试:搭建测试环境

下面将使用Spring Mock对宠物店的Web控制器ViewCartController进行单元测试,目的是展示Spring Mock的基本使用技巧。

为了正确搭建测试环境,给出ViewCartController的源代码以及相关的配置文件,如代码18.1~18.2所示。

代码18.1  ViewCartController.java

 

package org.springframework.samples.jpetstore.web.spring;

 

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

 

import org.springframework.samples.jpetstore.domain.Cart;

import org.springframework.web.servlet.ModelAndView;

import org.springframework.web.servlet.mvc.Controller;

import org.springframework.web.util.WebUtils;

 

public class ViewCartController implements Controller {

 

  private String successView;

  public void setSuccessView(String successView) {

    this.successView = successView;

  }

  public ModelAndView handleRequest(HttpServletRequest request,

                                    HttpServletResponse response)

                                    throws Exception {

    UserSession userSession =

      (UserSession) WebUtils.getSessionAttribute(request, "userSession");

    Cart cart =

      (Cart) WebUtils.getOrCreateSessionAttribute(request.getSession(),

                                                  "sessionCart", Cart.class);

    String page = request.getParameter("page");

    if (userSession != null) {

      if ("next".equals(page)) {

        userSession.getMyList().nextPage();

      }

      else if ("previous".equals(page)) {

        userSession.getMyList().previousPage();

      }

    }

    ...

    return new ModelAndView(this.successView, "cart", cart);

  }

}

 

代码18.2  petstore-servlet.xml

 

<?xml version="1.0" encoding="UTF-8" ?>

<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN"

"http://www.springframework.org/dtd/spring-beans.dtd">

 

<beans>

...

  <bean name="/shop/viewCart.do"

class="org.springframework.samples.jpetstore.web.spring.ViewCartController">

    <property name="successView" value="Cart"/>

  </bean>

...

</beans>

 

petstore-servlet.xml中的代码片断是关于ViewCartController的配置,为了真实的模拟单元测试环境,笔者在ch18/springmock目录下新建了一个petstore-servlet.xml,并复制了以上代码片段作为测试上下文。

说明:虽然本书经常将测试依赖于外部配置,但在真实环境下,使用配置文件作为测试上下文并不总是一个好的选择,因为需要维护日益增多的测试套件。

给出测试案例骨架,如代码18.3所示。

代码18.3  ViewCartControllerTest.java

 

package chapter18.springmock;

 

import java.util.ArrayList;

import java.util.Iterator;

import java.util.List;

 

import junit.framework.TestCase;

 

import org.springframework.beans.support.PagedListHolder;

import org.springframework.context.ApplicationContext;

import org.springframework.context.support.ClassPathXmlApplicationContext;

import org.springframework.mock.web.MockHttpServletRequest;

import org.springframework.mock.web.MockHttpServletResponse;

import org.springframework.mock.web.MockHttpSession;

import org.springframework.samples.jpetstore.domain.Account;

import org.springframework.samples.jpetstore.domain.Product;

import org.springframework.samples.jpetstore.web.spring.UserSession;

import org.springframework.samples.jpetstore.web.spring.ViewCartController;

import org.springframework.web.servlet.ModelAndView;

import org.springframework.web.util.WebUtils;

 

public class ViewCartControllerTest extends TestCase {

 

  private MockHttpServletRequest request;

  private MockHttpServletResponse response;

  private MockHttpSession session;

  private ViewCartController viewCartController;

 

  protected void setUp() throws Exception {

    request = new MockHttpServletRequest();

    response = new MockHttpServletResponse();

    session = (MockHttpSession)request.getSession();

    ApplicationContext context = new ClassPathXmlApplicationContext(

        "ch18/springmock/petstore-servlet.xml");

    viewCartController =

      (ViewCartController)context.getBean("/shop/viewCart.do");

  }

}

如代码18.3使用了MockHttpServletRequestMockHttpServletResponseMockHttpSession这三个组件,分别模拟真实Servlet环境下的HttpServletRequestHttpServletResponsHttpSession。可以发现,现在可以脱离Servlet环境,对Web组件进行单元测试了。

下文还将逐步对这些对象设定一些模拟值,以测试ViewCartController的一些预期行为。

18.3.2  Web组件的单元测试:视图转发

首先制定的测试案例是:ViewCartController是否依据输入参数进行了正确的视图转发。可以知道,在Spring MVC应用中,正确的视图信息被包含于ModelAndView对象。据此,添加测试方法如下:

  public void testViewCartSuccessView() throws Exception {

    request.setMethod("POST");

    //显式调用Controller组件的handleRequest()方法

    ModelAndView modelView =

      viewCartController.handleRequest(request, response);

 

    //petstore-servlet.xml/shop/viewCart.dosuccessView属性作为预期值

    String expectSuccessView = "Cart";

 

    assertEquals("正确的视图转发测试", expectSuccessView,

                                    modelView.getViewName());

  }

18.3.3  Web组件的单元测试:会话状态

接着制定的测试案例是:ViewCartController是否创建了正确的会话状态。观察代码18.1,可以发现,其中使用了Spring提供的一个Web工具类,它从HttpSession中取出或创建给定的属性值。另外,ViewCartController中还使用了一些领域对象。

注意:本测试方法中,领域对象的作用只是起了一个占位符的作用,简单地构造它们即可。

添加测试方法如下:

  /**

   * Session(会话)状态测试

   */

  public void testSessionWellAndCartPassedByModelKeyAutoCreated()

    throws Exception {

    //简单地构造领域对象

    UserSession userSession = new UserSession(new Account());

 

    //向模拟HttpSession设值

    session.setAttribute("userSession", userSession);

 

    //通过WebUtils取得预期UserSession对象

    UserSession expectUserSession =

      (UserSession)WebUtils.getSessionAttribute(request, "userSession");

 

    ModelAndView modelView =

      viewCartController.handleRequest(request, response);

 

    //测试HttpSession是否通过给定属性正确传递了UserSession对象

    assertSame("Session状态测试", userSession, expectUserSession);

 

    //测试Cart对象是否被自动创建于Session并且通过modelViewkey值正确传递

    Cart expectCart = (Cart)modelView.getModel().get("cart");

    assertTrue("SessionCart对象的自动创建测试",expectCart instanceof Cart);

  }

18.3.4  Web组件的单元测试:简单逻辑

现在制定业务逻辑的测试案例,说明如下:

1)是否依据给定的分页导向标识(如nextprevious…),触发了正确的处理脚本

2)是否依据给定的分页标识(如pageSize),进行了正确的分页处理

根据以上两点,添加测试方法如下:

  public void testMyListOfUserSessionPagable() {

    UserSession userSession = new UserSession( new Account());

    userSession.setMyList(mockProductList(9, 4));

    session.setAttribute("userSession", userSession);

    UserSession expectUserSession =

      (UserSession)WebUtils.getSessionAttribute(request, "userSession");

 

    //下一页测试

    request.setParameter("page", "next");

    String pageTurnedTo = request.getParameter("page");

    if (pageTurnedTo.equals("next")) {

      //首次翻至下页

      expectUserSession.getMyList().nextPage();

      //取得当前页的数据列表

      List expectPagedList = expectUserSession.getMyList().getPageList();

      //0开始计数并以每页4条记录起算

      //以上数据列表中预期的第一条数据ID4

      int productID = 4;

      for (Iterator iter = expectPagedList.iterator(); iter.hasNext();) {

        Product product = (Product)iter.next();

        System.out.println(product);

 

        assertEquals(product.getProductId(), String.valueOf(productID++));

      }

    }

    //前一页测试

    request.setParameter("page", "previous");

    pageTurnedTo = request.getParameter("page");

    int productID = 0;

    if (pageTurnedTo.equals("previous")) {

      expectUserSession.getMyList().previousPage();

      List expectPagedList = expectUserSession.getMyList().getPageList();

      for (Iterator iter = expectPagedList.iterator(); iter.hasNext();) {

        Product product = (Product)iter.next();

        System.out.println(product);

 

        assertEquals(product.getProductId(), String.valueOf(productID++));

      }

    }

  }

  /**

   * 生成模拟数据

   *

   * @param dataListSize: 模拟的数据记录条数

   * @param pageSize: 每页显示的记录条数

   */

  private PagedListHolder mockProductList(int dataListSize, int pageSize) {

    List productList = new ArrayList();

    for(int i = 0; i < dataListSize; i++) {

      Product product = new Product();

      product.setProductId(String.valueOf(i));

      product.setName("产品-"+i);

      productList.add(product);

    }

    //使用Spring的分页工具类PagedListHolder包装目标数据列表

    PagedListHolder pagedList = new PagedListHolder(productList);

    pagedList.setPageSize(pageSize);

    return pagedList;

  }

最后运行以上所有测试,效果如18.1

18.1  运用Spring Mock进行Web组件单元测试效果

控制台显示:

产品-4

产品-5

产品-6

产品-7

产品-0

产品-1

产品-2

产品-3

18.3.5 事务性单元测试:使用Spring Mock事务基类搭建测试环境

如上文所述,Spring Mock提供了一些便利的事务测试基类,使用它们可以方便地对业务组件进行事务性的单元测试。接下来将使用其中的AbstractTransactionalDataSourceSpringContextTests进行测试。

说明:AbstractTransactionalDataSourceSpringContextTestsAbstractTransactionalSpringContextTests的子类,它不但具有自动装配测试组件的功能,并且可以直接使用SpringJdbcTemplate来辅助测试。

为了配置一个最简的事务测试环境,给出如下配置文件和测试案例,如代码18.4~18.7所示。

代码18.4  applicationContext-tx-minimum.xml

 

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN"

"http://www.springframework.org/dtd/spring-beans.dtd">

 

<beans>

  <bean id="propertyConfigurer"

  class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">

    <property name="locations">

      <list>

        <value>classpath:jdbc.properties</value>

      </list>

    </property>

  </bean>

  <bean id="baseTransactionProxy"

  class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean"

      abstract="true">

    <property name="transactionManager" ref="transactionManager"/>

    <property name="transactionAttributes">

      <props>

        <prop key="insert*">PROPAGATION_REQUIRED</prop>

      </props>

    </property>

  </bean>

  <bean id="petStore" parent="baseTransactionProxy">

    <property name="target">

      <bean class="org.springframework.samples.jpetstore.domain.logic.PetStoreImpl">

        <property name="accountDao" ref="accountDao"/>

      </bean>

    </property>

  </bean>

</beans>

 

代码18.5  dataAccessContext-local-minimum.xml

 

 

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN"

"http://www.springframework.org/dtd/spring-beans.dtd">

 

<beans>

  <bean id="dataSource"

  class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">

    <property name="driverClassName" value="${jdbc.driverClassName}"/>

    <property name="url" value="${jdbc.url}"/>

    <property name="username" value="${jdbc.username}"/>

    <property name="password" value="${jdbc.password}"/>

  </bean>

 

  <bean id="sqlMapClient"

  class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">

    <property name="configLocation"

    value="classpath:ch18/springmock/sql-map-config-minimum.xml"/>

    <property name="dataSource" ref="dataSource"/>

  </bean>

 

  <bean id="transactionManager"

  class="org.springframework.jdbc.datasource.DataSourceTransactionManager">

    <property name="dataSource" ref="dataSource"/>

  </bean>

 

  <bean id="accountDao"

  class="org.springframework.samples.jpetstore.dao.ibatis.SqlMapAccountDao">

    <property name="sqlMapClient" ref="sqlMapClient"/>

  </bean>

 

</beans>

 

代码18.6  sql-map-config-minimum.xml

 

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE sqlMapConfig PUBLIC "-//iBATIS.com//DTD SQL Map Config 2.0//EN"

    "http://www.ibatis.com/dtd/sql-map-config-2.dtd">

 

<sqlMapConfig>

  <sqlMap resource="org/springframework/samples/jpetstore/dao/ibatis/maps/Account.xml"/>

</sqlMapConfig>

 

代码18.7  PetStoreFacadeTransactionTest.java

 

package chapter18.springmock;

 

import org.springframework.samples.jpetstore.domain.Account;

import org.springframework.samples.jpetstore.domain.logic.PetStoreFacade;

import org.springframework.test.AbstractTransactionalDataSourceSpringContextTests;

 

public class PetStoreFacadeTransactionTest

  extends AbstractTransactionalDataSourceSpringContextTests {

  private static final String USERNAME = "Spirit.J";

  private static final String TEST_SQL =

    "SELECT COUNT(*) FROM ACCOUNT WHERE USERID='"+USERNAME+"'";

 

  private PetStoreFacade petStore;

  public void setPetStore(PetStoreFacade petStore) {

    this.petStore = petStore;

  }

 

  protected String[] getConfigLocations() {

    String path = "classpath:ch18/springmock/";

    return new String[]{path+"applicationContext-tx-minimum.xml",

        path+"dataAccessContext-local-minimum.xml"};

  }

 

  public void testPetStoreTransactionWorking() {

    Account account = new Account();

    account.setUsername(USERNAME);

    account.setPassword("1111");

    account.setEmail("fvyaba@126.com");

    account.setFirstName("Spirit.");

    account.setLastName("J");

    account.setAddress1("inlet");

    account.setCity("Shanghai");

    account.setState("ok");

    account.setZip("1111");

    account.setCountry("China");

    account.setPhone("1111");

    account.setLanguagePreference("CN");

    petStore.insertAccount(account);

    int result =

      jdbcTemplate.queryForInt(TEST_SQL);

    assertEquals("事务进行中测试", 1, result);

  }

 

  protected void onTearDownAfterTransaction() throws Exception {

    int result =

      jdbcTemplate.queryForInt(TEST_SQL);

    assertEquals("事务性单元测试", 0, result);

  }

}

说明如下:

1)代码18.7②处的getConfigLocations()是必须实现的基类抽象方法,它用以载入测试的上下文配置。

2)所谓的自动装配,就如代码18.418.7的①处,只要发现上下文配置中,有与测试组件属性相匹配的Bean id或者name,就会自动进行注射。

3)代码18.7可以直接使用jdbcTemplate,这是父类提供的贴心帮助

4)默认的,AbstractTransactionalSpringContextTests将在这个测试方法结束后,实现自动回滚。所以,在事务结束后,如代码18.7 onTearDownAfterTransaction()方法中,预期插入表中的记录数应该是0

最后,为了正确运行测试,请安装Postgres数据库,启动服务并导入jpetstore-postgres-schema.sql