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使用了MockHttpServletRequest、MockHttpServletResponse、MockHttpSession这三个组件,分别模拟真实Servlet环境下的HttpServletRequest、HttpServletRespons和HttpSession。可以发现,现在可以脱离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.do的successView属性作为预期值
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并且通过modelView的key值正确传递
Cart expectCart = (Cart)modelView.getModel().get("cart");
assertTrue("Session中Cart对象的自动创建测试",expectCart instanceof Cart);
}
18.3.4 Web组件的单元测试:简单逻辑
现在制定业务逻辑的测试案例,说明如下:
(1)是否依据给定的分页导向标识(如next、previous…),触发了正确的处理脚本
(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条记录起算
//以上数据列表中预期的第一条数据ID是4
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进行测试。
说明:AbstractTransactionalDataSourceSpringContextTests是AbstractTransactionalSpringContextTests的子类,它不但具有自动装配测试组件的功能,并且可以直接使用Spring的JdbcTemplate来辅助测试。
为了配置一个最简的事务测试环境,给出如下配置文件和测试案例,如代码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.4和18.7的①处,只要发现上下文配置中,有与测试组件属性相匹配的Bean id或者name,就会自动进行注射。
(3)代码18.7可以直接使用jdbcTemplate,这是父类提供的贴心帮助
(4)默认的,AbstractTransactionalSpringContextTests将在这个测试方法结束后,实现自动回滚。所以,在事务结束后,如代码18.7的 onTearDownAfterTransaction()方法中,预期插入表中的记录数应该是0。
最后,为了正确运行测试,请安装Postgres数据库,启动服务并导入jpetstore-postgres-schema.sql。






