EasyMock简介

Spring Mock Web简介

Spring针对J2EE的常用Web接口提供了Mock,这些组件被发布于spring-mock.jar,介绍如下:

q        MockHttpServletRequest:HttpServletRequest接口的mock实现。

q        MockHttpServletResponse:HttpServletResponse接口的mock实现。

q        MockHttpSession:HttpSession接口的mock实现。

q        DelegatingServletInputStream:ServletInputStream接口的委托mock实现。

q        DelegatingServletOutputStream: ServletOutputStream接口的委托mock实现,在需要拦截和分析写向一个输出流的内容时,可以使用它。

说明:在提供关于Controller(控制器)的测试时,以上这些对象是最常用的。

此外,Spring还提供了一些其它组件的mock实现:

q        MockExpressionEvaluator:基于JSTL的定制标签库的mock实现

q        MockFilterConfig:FilterConfig接口的mock实现。

q        MockPageContext:JSP PageContext接口的mock实现,用于测试预编译的JSP。

q        MockRequestDispatcher:RequestDispatcher接口的mock实现,它主要结合其它Mock来使用。

q        MockServletConfig:ServletConfig接口的mock实现,在测试某类(如Struts框架)所提供的Web组件时,要求设置由MockServletContext所实现的ServletConfig和ServletContext接口

以上这些Mock位于“org.springframework.mock.web”包下,下一节中将看到Spring Mock所提供的其它功能。

扩展JUnit框架的测试基类

除了上节中介绍的Web Mock,Spring Mock包还提供了一些扩展自JUnit框架的测试基类,这些基类简化了对依赖注射和事务管理的单元测试,介绍如下:

q      AbstractDependencyInjectionSpringContextTests:依赖于Spring Context组件的测试基类。

q      AbstractTransactionalSpringContextTests:与事务相关的测试基类。

说明:AbstractTransactionalSpringContextTests定制的行为方式是对事务进行正常回滚。在实际使用时,需要重载onSetUpInTransaction()和onTearDownInTransaction()方法,以手工开始并提交事务。

q      AbstractTransactionalDataSourceSpringContextTests:使用了Spring的JdbcTemplate来辅助测试,它是AbstractTransactionalSpringContextTests的子类。

以上这些Mock位于“org.springframework.test”包下。

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

EasyMock是一个Mock对象的类库。本书使用的EasyMock版本是2.2,可在http://sourceforge.net/projects/easymock下载到。

Mock 对象能够模拟领域对象的部分行为,并且能够检验运行结果是否和预期的一致。领域类将通过与Mock 对象的交互,来获得一个独立的测试环境。EasyMock可以动态地生成Mock 对象而不需要编写它们,因为也不会产生多余代码。

说明:默认情况下,EasyMock只支持为接口生成Mock。如果还需要为类生成Mock,可在上述网址下载EasyMock的扩展包以完成此功能。本书使用的EasyMock扩展包是2.2.1版本,它只能在Java 5.0以上的版本中运行。

一、组件单元测试:搭建测试环境

为了搭建一个真实的测试环境,接下来将使用EashMock框架来模拟宠物店的业务核心接口PetStoreFacade,并模拟实现其子类PetStoreImpl的行为,其中涉及和一些相关领域对象、DAO的交互。

创建测试骨架,如代码1所示。

代码1  PetStoreFacadeTest.java

 

package chapter.easymock;

 

import static org.easymock.EasyMock.createMock;

import static org.easymock.EasyMock.expectLastCall;

import static org.easymock.EasyMock.verify;

import static org.easymock.EasyMock.replay;

 

import org.easymock.classextension.EasyMock;

 

import junit.framework.TestCase;

 

import org.springframework.samples.jpetstore.dao.AccountDao;

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

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

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

 

public class PetStoreFacadeTest extends TestCase {

 

  private PetStoreFacade petStoreFacade;

  private PetStoreImpl petStoreImpl;

  private AccountDao mockAccountDao;

 

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

  private static final String PASSWORD = "1111";

 

  public void setUp() {

    petStoreFacade = createMock(PetStoreFacade.class);

    petStoreImpl = EasyMock.createMock(PetStoreImpl.class);

    mockAccountDao = createMock(AccountDao.class);

  }

}

说明如下:

(1)①处使用了Java5.0的一种新特性:静态引入。它允许对静态方法的直接调用,如③处所示。

(2)②处比较特别,由于下文示例中需要Mock具体类而非接口,所以必须使用EashMock扩展包所提供的EasyMock类,它的使用方法如④处所示。请注意它和接口Mock的区别。

说明:通过EasyMock的createMock()方法,几乎可以模仿任何接口或类。在早期的EasyMock中,createMock()是MockControl类上的静态方法,而现在MockControl类已经废弃了。

二、组件单元测试:模拟业务接口和领域对象的交互

一般情况下,业务接口需要和领域对象进行交互,使用EasyMock可以轻松模拟出类似这样的实现。添加测试方法如下:

  public void testFacadeWithDomainObject() throws Exception {

    Account account = new Account();

    account.setUsername(USERNAME);

    account.setPassword(PASSWORD);

 

    petStoreFacade.insertAccount(account);

    expectLastCall().once();

    replay(petStoreFacade);

 

    petStoreFacade.insertAccount(account);

    verify(petStoreFacade);

  }

说明如下:

(1)Account是最简单的领域对象,它只具有getter/setter方法,创建它以备用。

(2)在①处,记录了对象的预期行为;②处下达了一个期望:希望该行为只被执行一次;③处通过调用静态引入的replay()方法,激活了这个PetStoreFacade模拟对象。

说明:通常要得到一个Mock对象,最简步骤为:为想要模拟的接口创建一个Mock对象(如代码4中的setUp()方法),记录预期的行为(如上述代码的①处),然后将Mock对象切换到replay状态(如上述代码的③处)。

(3)激活后,petStoreFacade就真正成为了PetStoreFacade接口的Mock对象句柄了,如果不在②处对它的预期行为加以修饰,那么在④的后续调用是不合法的。

(4)在调用replay()方法之前的状态,EashMock称之为“record状态”。该状态下,Mock对象不具备行为(即模拟接口的实现),它仅仅记录方法的调用。在调用replay()后,它才以Mock对象预期的行为进行工作,检查预期的方法调用是否真的完成。

(5)经过Mock的创建和激活,④处模拟了一次真实的业务接口调用。在⑤处出现的verify()也并不神秘,它只是用来验证PetStoreFacade接口上的insertAccount()方法是否真的被调用了。

有了以上的测试后,可以发现,PetStoreFacade接口上的insertAccount()方法是不具返回值的。现在来看对返回值的测试,添加如下测试方法:

  public void testFacadeReturnValue() throws Exception {

    Account expectAccount = new Account();

    expectAccount.setUsername(USERNAME);

    expectAccount.setPassword(PASSWORD);

 

    petStoreFacade.getAccount(USERNAME);

    expectLastCall().andReturn(expectAccount);

//expect(petStoreFacade.getAccount(USERNAME)).andReturn(expectAccount);

    replay(petStoreFacade);

 

    Account returnAccount = petStoreFacade.getAccount(USERNAME);

    assertNotNull("返回值测试", returnAccount);

  }

①和②处的写法是等价的,它们用以描述getAccount()方法的预期行为,即返回一个Account对象。

三、组件单元测试:模拟具体类和DAO的交互

在真实的应用场景中,DAO会被注入具体的业务对象,以接受业务对象的持久委托。据此,添加测试方法如下:

  public void testFacadeImplWithDao() throws Exception {

    Account expectAccount = new Account();

    expectAccount.setUsername(USERNAME);

    expectAccount.setPassword(PASSWORD);

 

    petStoreImpl.setAccountDao(mockAccountDao);

    petStoreImpl.getAccount(USERNAME);

    EasyMock.expectLastCall().andReturn(expectAccount);

    EasyMock.replay(petStoreImpl);

 

    Account returnAccount = petStoreImpl.getAccount(USERNAME);

    assertNotNull("返回值测试", returnAccount);

  }

可以看到,petStoreImpl是指向具体实现类(PetStoreImpl)的模拟对象句柄,通过调用它的setAccountDao()方法,注入了模拟的Dao对象。

以下的测试步骤和上文类似,其中省略了verify()方法。最后,运行以上三个测试,效果如图2所示。

图2  运用EasyMock进行组件单元测试效果

 

本节将结合使用Spring Mock和EasyMock,对宠物店的Web组件SignonController,以及业务核心接口PetStoreFacade进行单元测试。

四、 Web组件单元测试:模拟控制器和业务接口、领域对象的交互

为了正确给出SignonController的单元测试脚本,首先给出SignonController、PetStoreFacade及PetStoreImpl的源码,如代码所示。

代码  SignonController.java

 

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

 

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

 

import org.springframework.beans.support.PagedListHolder;

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

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

import org.springframework.web.servlet.ModelAndView;

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

 

public class SignonController implements Controller {

 

  private PetStoreFacade petStore;

 

  public void setPetStore(PetStoreFacade petStore) {

    this.petStore = petStore;

  }

  public ModelAndView handleRequest(HttpServletRequest request,

                                           HttpServletResponse response)

                                           throws Exception {

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

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

    Account account = this.petStore.getAccount(username, password);

    if (account == null) {

      return new ModelAndView("Error", "message",

                     "Invalid username or password.  Signon failed.");

    }

    else {

      UserSession userSession = new UserSession(account);

      PagedListHolder myList =

        new PagedListHolder(this.petStore.getProductListByCategory(

account.getFavouriteCategoryId()));

      myList.setPageSize(4);

      userSession.setMyList(myList);

      request.getSession().setAttribute("userSession", userSession);

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

      if (forwardAction != null) {

        response.sendRedirect(forwardAction);

        return null;

      }

      else {

        return new ModelAndView("index");

      }

    }

  }

}

代码10  PetStoreFacade.java

 

package org.springframework.samples.jpetstore.domain.logic;

 

import java.util.List;

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

...

 

public interface PetStoreFacade {

  ...

  Account getAccount(String username);

  ...

  List getProductListByCategory(String categoryId);

  ...

 

}

 

代码11  PetStoreImpl.java

 

package org.springframework.samples.jpetstore.domain.logic;

 

import java.util.List;

import org.springframework.samples.jpetstore.dao.AccountDao;

...

import org.springframework.samples.jpetstore.dao.ProductDao;

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

...

 

public class PetStoreImpl implements PetStoreFacade... {

  ...

  private AccountDao accountDao;

  ...

  private ProductDao productDao;

  ...

  public void setAccountDao(AccountDao accountDao) {

    this.accountDao = accountDao;

  }

  ...

  public void setProductDao(ProductDao productDao) {

    this.productDao = productDao;

  }

  ...

  public Account getAccount(String username, String password) {

    return this.accountDao.getAccount(username, password);

  }

  ...

  public List getProductListByCategory(String categoryId) {

    return this.productDao.getProductListByCategory(categoryId);

  }

  ...

}

 

代码9的①和②处分别是SignonController和业务接口以及领域对象发生交互的入口。可以发现,业务接口方法调用的返回值应该被作为测试的集中点。

据此添加测试案例,如代码12所示。

代码12  SignonControllerTest.java

 

package chapter.springandeasymock;

 

import static org.easymock.EasyMock.createMock;

import static org.easymock.EasyMock.expectLastCall;

 

import java.util.ArrayList;

import java.util.List;

 

import junit.framework.TestCase;

 

import org.easymock.classextension.EasyMock;

import org.springframework.mock.web.MockHttpServletRequest;

import org.springframework.mock.web.MockHttpServletResponse;

import org.springframework.samples.jpetstore.dao.AccountDao;

import org.springframework.samples.jpetstore.dao.ProductDao;

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

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

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

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

import org.springframework.web.servlet.ModelAndView;

 

public class SignonControllerTest extends TestCase {

  private MockHttpServletRequest request;

  private MockHttpServletResponse response;

  private AccountDao mockAccountDao;

  private ProductDao mockProductDao;

  private PetStoreImpl petStore;

  private SignonController controller;

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

  private static final String PASSWORD = "1111";

 

  protected void setUp() throws Exception {

    request = new MockHttpServletRequest();

    response = new MockHttpServletResponse();

    mockAccountDao = createMock(AccountDao.class);

    mockProductDao = createMock(ProductDao.class);

    petStore = EasyMock.createMock(PetStoreImpl.class);

    controller = new SignonController();

  }

  /**

   * 取得Account为空测试

   */

  public void testAccountNull() throws Exception {

    mockPetStoreImplAndControllerSetting(true);

    ModelAndView modelView =

      controller.handleRequest(request, response);

 

    assertEquals("Account为空测试", "Error", modelView.getViewName());

    assertEquals("错误消息测试",

                 "Invalid username or password.  Signon failed.",

                 modelView.getModel().get("message"));

 

  }

  /**

   * 正确进入Index页面测试

   */

  public void testAccountNotNullAndGoIndex() throws Exception {

    mockPetStoreImplAndControllerSetting(false);

    ModelAndView modelView =

      controller.handleRequest(request, response);

    assertEquals("正确进入Index页面测试", "index", modelView.getViewName());

  }

  /**

   * 事先录入PetStoreImpl的一些预期行为,并向Controller注射被激活的模拟对象

   */

  private void mockPetStoreImplAndControllerSetting(

      boolean isAccountNull) {

 

    //预期的领域对象初始化

    String favouriteCategoryId = "9999";

    Account expectAccount = new Account();

    List expectProductList = new ArrayList();

    expectAccount.setUsername(USERNAME);

    expectAccount.setPassword(PASSWORD);

    expectAccount.setFavouriteCategoryId(favouriteCategoryId);

    //注射MockDao

    ((PetStoreImpl)petStore).setAccountDao(mockAccountDao);

    ((PetStoreImpl)petStore).setProductDao(mockProductDao);

 

    if (isAccountNull) expectAccount = null;

 

    //录入业务接口的预期行为并描述其返回值

    petStore.getAccount(USERNAME, PASSWORD);

    expectLastCall().andReturn(expectAccount);

    petStore.getProductListByCategory(favouriteCategoryId);

    expectLastCall().andReturn(expectProductList);

 

    //激活Mock

    EasyMock.replay(petStore);

    //controller注射Mock

    controller.setPetStore(petStore);

    //请求参数初始化

    request.setParameter("username", USERNAME);

    request.setParameter("password", PASSWORD);

  }

}

五、Web组件单元测试:重定向测试

观察代码9 SignonController的③处,为了测试重定向行为,简单地向上述测试案例中,添加如下测试方法:

  /**

   * 重定向测试

   */

  public void testRedirect() throws Exception {

    mockPetStoreImplAndControllerSetting(false);

    request.setParameter("forwardAction", "RedirectView");

    ModelAndView modelView =

      controller.handleRequest(request, response);

 

    assertEquals("重定向URL测试", "RedirectView",

                 response.getRedirectedUrl());

    assertNull("重定向返回值测试", modelView);

  }

 

 

本文章主要围绕Spring和单元测试,讲解了模仿对象的概念以及如何使用Spring MockEasyMock来进行单元测试。其中分别涉及了Web组件、业务组件、领域对象以及事务性组件的单元测试技巧。可以看到,模仿对象在单元测试中有着非常大的价值。

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

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

代码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);

  }

}

 

代码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,并复制了以上代码片段作为测试上下文。

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

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

代码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");

  }

}

如代码3使用了MockHttpServletRequest、MockHttpServletResponse、MockHttpSession这三个组件,分别模拟真实Servlet环境下的HttpServletRequest、HttpServletRespons和HttpSession。可以发现,现在可以脱离Servlet环境,对Web组件进行单元测试了。

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

 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());

  }

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

接着制定的测试案例是:ViewCartController是否创建了正确的会话状态。观察代码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);

  }

 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条记录起算

      //以上数据列表中预期的第一条数据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;

  }

最后运行以上所有测试,效果如图1:

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

控制台显示:

产品-4

产品-5

产品-6

产品-7

产品-0

产品-1

产品-2

产品-3

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

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

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

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

代码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>

 

代码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>

 

代码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>

 

代码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)代码7②处的getConfigLocations()是必须实现的基类抽象方法,它用以载入测试的上下文配置。

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

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

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

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

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值