Google

Dec 13, 2012

Unit testing Spring MVC controllers for web and RESTful services with spring-test-mvc

As Web application and RESTful web services are very common in enterprise applications, it is imperative to unit test the controllers. There are a number of strategies to unit test your controllers in an MVC framework. For example, running an embedded web server like jetty, and then run the unit tests against the web server, etc. The spring-test-mvc project makes testing your Spring MVC controllers very easy without starting an embedded server. This blog post will take you through the key steps involved.


Step 1: You need to have the right third-party libraries. The key ones to take note are spring-test-mvc, spring-test, json-path, hamcrest-library, hamcrest-core, and mockito-core.

 
        <dependency>
   <groupId>org.mockito</groupId>
   <artifactId>mockito-core</artifactId>
   <scope>test</scope>
   <exclusions>
    <exclusion>
     <groupId>org.hamcrest</groupId>
     <artifactId>hamcrest-core</artifactId>
    </exclusion>
   </exclusions>
  </dependency>
  
  <dependency>
   <groupId>org.hamcrest</groupId>
   <artifactId>hamcrest-library</artifactId>
   <version>1.3</version>
  </dependency>
  
  <dependency>
   <groupId>org.codehaus.jackson</groupId>
   <artifactId>jackson-mapper-asl</artifactId>
   <version>1.9.8</version>
   <scope>runtime</scope>
  </dependency>

  <dependency>
   <groupId>javax.xml.bind</groupId>
   <artifactId>jaxb-api</artifactId>
   <version>2.2.6</version>
   <scope>runtime</scope>
  </dependency>

  <!-- JSON XPATH library -->
  <dependency>
   <groupId>com.jayway.jsonpath</groupId>
   <artifactId>json-path</artifactId>
   <version>0.5.6</version>
  </dependency>
  
  <!-- spring -->
  
  <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>org.springframework.context</artifactId>
   <version>3.1.0.RELEASE</version>
   <exclusions>
    <!-- Exclude Commons Logging in favour of SLF4j -->
    <exclusion>
     <groupId>org.apache.commons</groupId>
     <artifactId>com.springsource.org.apache.commons.logging</artifactId>
    </exclusion>
   </exclusions>
  </dependency>
  <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-aop</artifactId>
   <version>3.1.0.RELEASE</version>
  </dependency>
  <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-aspects</artifactId>
   <version>3.1.0.RELEASE</version>
  </dependency>
  <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-beans</artifactId>
   <version>3.1.0.RELEASE</version>
  </dependency>
  <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-core</artifactId>
   <version>3.1.0.RELEASE</version>
   <exclusions>
    <exclusion>
     <artifactId>commons-logging</artifactId>
     <groupId>commons-logging</groupId>
    </exclusion>
   </exclusions>
  </dependency>
  
  <!-- for tspring testing -->
  <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-test</artifactId>
   <version>3.1.0.RELEASE</version>
  </dependency>
  
  <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-test-mvc</artifactId>
   <version>1.0.0.M1</version>
  </dependency>

  
  

Step 2: The next step is to define a Spring MVC controller that we will be writing unit test for.

 
package com.myapp.accounting.aes.securities.pricehistory.controller;

import java.sql.SQLException;
import java.util.Date;
import java.util.List;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.http.HttpStatus;
import org.springframework.jdbc.CannotGetJdbcConnectionException;
import org.springframework.security.authentication.LockedException;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;

import com.myapp.accounting.securities.pricehistory.model.PriceHistory;
import com.myapp.accounting.securities.pricehistory.service.PriceHistoryService;
import com.myapp.accounting.dm.model.refdata.securities.MarketDataSecurityPriceHist;
 

 @Controller
public class PriceHistoryController {
 
    /**
     * Service for accessing the Market Data repository. Contains the business logic.
     */
    @Resource(name = "aes_priceHistoryService")
    private PriceHistoryService securityPriceService;


 @RequestMapping(value = "/pricehistory", 
                 method = RequestMethod.GET)
 @ResponseBody
 public List<MarketDataSecurityPriceHist> getPriceHistory(@RequestParam(value="latestUpdatedTimeStamp", required=true) @DateTimeFormat(pattern="dd MMM yyyy HH:mm:ss") Date latestUpdatedTimeStamp,
                                                     @RequestParam(value="maxRecords", required=false, defaultValue="100") int maxRecords) throws Exception 
 {  
     // call the business service
     PriceHistory result = securityPriceService.getPriceHistory(maxRecords, latestUpdatedTimeStamp);
      
     return result.getHistory();
 }
}


Step 3: The PriceHistory object that holds a list of "MarketDataSecurityPriceHist" objects.

 
package com.myapp.accounting.aes.securities.pricehistory.model;

import com.myapp.accounting.dm.model.refdata.securities.MarketDataSecurityPriceHist;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementWrapper;
import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement
public class PriceHistory implements Serializable
{
    private static final long serialVersionUID = 1L;
    

    private List<marketdatasecuritypricehist> history = new ArrayList<marketdatasecuritypricehist>();

    @XmlElementWrapper(name = "historicalprices")
    @XmlElement(name = "price")
    public List<marketdatasecuritypricehist> getHistory()
    {
        return history;
    }

 public void setHistory(List history) {
  this.history = history;
 }
}


Step 4: Define the MarketDataSecurityPriceHist class that holds the relevant attributes.

 
package com.myapp.accounting.dm.model.refdata.securities;

import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;

import javax.persistence.Entity;
import javax.xml.bind.annotation.XmlElement;

@Entity //persistence entity
public class MarketDataSecurityPriceHist implements Serializable 
{
 private static final long serialVersionUID = 1444498748039607997L;

    private String securityCd;
    private Date priceDttm;
    private BigDecimal bidPrice;
    private BigDecimal closePrice;
    private BigDecimal lastPrice;
    private BigDecimal askPrice;
    private String lastUpdatedAction;
    private String LastUpdatedDetailUser;
    private Date lastUpdatedTimeStamp;
    private Date loadTimeStamp;
    private String pricecondition;
    private String recordType;
    private String priceTime;
    private String source;
    
 public MarketDataSecurityPriceHist()  {}

 @XmlElement
 public String getSecurityCd() 
 {
  return securityCd;
 }

 public void setSecurityCd(String securityCd) 
 {
  this.securityCd = securityCd;
 }

 @XmlElement
 public Date getPriceDttm() {
  return priceDttm;
 }

 public void setPriceDttm(Date priceDttm) {
  this.priceDttm = priceDttm;
 }
 
 @XmlElement
 public BigDecimal getBidPrice() {
  return bidPrice;
 }

 public void setBidPrice(BigDecimal bidPrice) {
  this.bidPrice = bidPrice;
 }

 //other setters and getters are omitted 
}


Step 5: Finally, and most importantly the unit test class that uses mockito to mock the PriceHistoryService implementation and spring-test-mvc to mock the controller that returns a json response.

 
package com.myapp.accounting.aes.securities.pricehistory;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.server.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.server.setup.MockMvcBuilders.standaloneSetup;
import static org.springframework.test.web.server.result.MockMvcResultMatchers.*;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;

import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.http.MediaType;

import com.myapp.accounting.aes.securities.pricehistory.controller.PriceHistoryController;
import com.myapp.accounting.aes.securities.pricehistory.model.PriceHistory;
import com.myapp.accounting.aes.securities.pricehistory.service.PriceHistoryService;
import com.myapp.accounting.aes.securities.pricehistory.service.impl.PriceHistoryServiceImpl;
import com.myapp.accounting.dm.model.refdata.securities.MarketDataSecurityPriceHist;

public class PriceHistoryController2Test {

 private PriceHistoryService mockSecurityPriceService;
 private PriceHistoryController controller;

 @Before
 public void setup() {
  controller = new PriceHistoryController();
  mockSecurityPriceService = mock(PriceHistoryServiceImpl.class);
  controller.setSecurityPriceService(mockSecurityPriceService);
 }

 @Test
 public void getPriceHistory() throws Exception {

  when(
    mockSecurityPriceService.getPriceHistory(
      (Integer) Mockito.any(), (Date) Mockito.any()))
    .thenReturn(getPriceHistoryTestData());

  standaloneSetup(controller)
    .build()
    .perform(
      get(
        "/pricehistory?latestUpdatedTimeStamp=26 Jul 211 12:00:00&maxRecords=3000")
        .accept(MediaType.APPLICATION_JSON))
    .andExpect(status().isOk())
    .andExpect(content().type("application/json"))
    .andExpect(jsonPath("$.[0].securityCd").value("test"))
    .andExpect(jsonPath("$.[0].bidPrice").value(12.50))
          .andExpect(jsonPath("$.[0].closePrice").value(12.50));
         
 }

 //mock data that gets returned when getPriceHistory(...) method is called
 private static PriceHistory getPriceHistoryTestData() {

  PriceHistory ph = new PriceHistory();
  List<marketdatasecuritypricehist> history = new ArrayList<marketdatasecuritypricehist>();
  MarketDataSecurityPriceHist md = new MarketDataSecurityPriceHist();
  md.setSecurityCd("test");
  BigDecimal price = new BigDecimal("12.50");
  md.setAskPrice(price);
  md.setBidPrice(price);
  md.setClosePrice(price);
  md.setLastUpdatedAction("SOME_ACTION");
  md.setRecordType("RECORD_TYPE");
  md.setLastUpdatedDetailUser("tesr");
  Calendar cal = Calendar.getInstance();
  cal.set(2010, 01, 01, 00, 00, 00);
  Date date = cal.getTime();
  md.setLastUpdatedTimeStamp(date);
  md.setPriceDttm(date);
  System.out.println(date);
  
  history.add(md);
  ph.setHistory(history);

  return ph;
 }

}

That's all to it for testing an MVC controller. The URL for above JSON RESTful web service will be something like

 
http://localhost:8080/accounting-server/securities/pricehistory?latestUpdatedTimeStamp=26+Jul+2010+12:00:00&maxRecords=3000

and the JSON data returned will be something like

 
[{"securityCd":"XX123","priceDttm":"2012-01-28","bidPrice":125.50,"closePrice":126.60,"lastPrice":124.50,"askPrice":123.80,"lastUpdatedAction":"NO ACTION","lastUpdatedTimeStamp":1327705288015,"loadTimeStamp":88150,"pricecondition":"NO_CONDITION","recordType":null,"priceTime":"10:01:28.150 AM","source":"HiPort","lastUpdatedDetailUser":"arul"},
{"securityCd":"YY321","priceDttm":"2012-01-28","bidPrice":125.50,"closePrice":126.60,"lastPrice":124.50,"askPrice":123.80,"lastUpdatedAction":"NO ACTION","lastUpdatedTimeStamp":1327705288015,"loadTimeStamp":88150,"pricecondition":"NO_CONDITION","recordType":null,"priceTime":"10:01:28.150 AM","source":"HiPort","lastUpdatedDetailUser":"arul"}]




Labels: , ,

1 Comments:

Anonymous Anonymous said...

This comment has been removed by a blog administrator.

4:29 PM, December 13, 2012  

Post a Comment

Subscribe to Post Comments [Atom]

<< Home