Creating unit tests for Spring based web objects such as Controllers, Filters and Servlets is straightforward when using Spring's Mock objects. Spring provides a number of Mock objects which support key J2EE interfaces, such as MockHttpServletRequest, MockHttpServletResponse, MockHttpSession, MockFilterConfig and MockServletConfig. These mock objects allow tests to manipulate and control many aspects of these interfaces. As such, they are not merely proxied interfaces, but have enough internal functionality to simulate these important interfaces to allow for both initialization, with respect to MockHttpServletRequest and MockHttpSession, and response data verification, with respect to MockHttpServletResponse.
This post will focus on providing concrete examples of how to use these objects by examining existing Elastic Path test classes.
EditAccountFormControllerImpl
For the first example we will go through the some test cases necessary to test an existing form controller, EditAccountFormControllerImpl. There are two aspects of this controller we should test, the population of the form backing object and the account update on submit. Spring's Mock objects will help in both cases by setting up the session with a mocked shopping cart.
Form Backing Object Test (Setting request attributes)
The following test class makes use of MockHttpSession to hold a reference to a mocked shopping cart from which the Customer object will be obtained and used to populate the EditAccountFormBean whose values will be asserted.
/**
* Tests that the form backing object returns the right customer data in
* the form bean.
*/
@Test
public void testFormBackingObjectSetsFormBean() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.getSession().setAttribute(WebConstants.SHOPPING_CART, cart);
context.checking(new Expectations() {
{
oneOf(customer).getFirstName(); will(returnValue(FIRST_NAME));
oneOf(customer).getLastName(); will(returnValue(LAST_NAME));
oneOf(customer).getEmail(); will(returnValue(EMAIL));
oneOf(customer).getPhoneNumber(); will(returnValue(PHONE_NUMBER));
}
});
Object object = controller.formBackingObject(request);
assertTrue("Form backing object is not instance of EditAccountFormBean",
object instanceof EditAccountFormBean);
EditAccountFormBean bean = (EditAccountFormBean) object;
assertEquals("First name does not match", FIRST_NAME, bean.getFirstName());
assertEquals("Last name does not match", LAST_NAME, bean.getLastName());
assertEquals("Email does not match", EMAIL, bean.getEmail());
assertEquals("Phone number does not match", PHONE_NUMBER, bean.getPhoneNumber());
}
On Submit Test (Testing for errors)
In this test we will again use the Spring mock objects to setup the session with the shopping cart in order to test submitting a duplicate email address. We also make use of a BindException to capture and test errors generated as well as inspecting the returned ModelAndView to ensure the view is correct.
/**
* Tests the situation where the modified email already exists.
*/
@Test
public void testOnSubmitWithDuplicateException() {
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
request.getSession().setAttribute(WebConstants.SHOPPING_CART, cart);
EditAccountFormBean bean = createFormBean();
final BindException bindException = new BindException(bean, "editAccount");
context.checking(new Expectations() {
{
oneOf(customer).setFirstName(with(FIRST_NAME));
oneOf(customer).setLastName(with(LAST_NAME));
oneOf(customer).setEmail(with(EMAIL));
oneOf(customer).setPhoneNumber(with(PHONE_NUMBER));
oneOf(customerService).update(customer); will(throwException(new UserIdExistException(TEST_MSG)));
}
});
ModelAndView view = controller.onSubmit(request, response, bean, bindException);
assertEquals("View name was not form view.", FORM_VIEW, view.getViewName());
assertTrue("Field error is missing.", bindException.hasFieldErrors(EMAIL));
}
LocaleControllerImpl (Setting request headers)
This example will show how to set up request parameters and a request header in the Mock request. It also demonstrates how to extract the URL of a RedirectView for assertion purposes.
/**
* Tests setting locale, currency and referrer parameters.
*
* @throws Exception a controller exception
*/
@Test
public void testSettingLocaleParameters() throws Exception {
final MockHttpServletRequest request = new MockHttpServletRequest();
final MockHttpServletResponse response = new MockHttpServletResponse();
request.getSession().setAttribute(WebConstants.SHOPPING_CART, cart);
request.addParameter(WebConstants.LOCALE_ID, Locale.CANADA.toString());
request.addParameter(WebConstants.CURRENCY, Currency.getInstance(Locale.CANADA).getCurrencyCode());
request.addHeader("Referer", REFERER_URL);
context.checking(new Expectations() {
{
allowing(requestHelper).getShoppingCart(request); will(returnValue(cart));
allowing(requestHelper).setShoppingCart(request, cart);
oneOf(cart).setLocale(Locale.CANADA);
oneOf(cart).setCurrency(Currency.getInstance(Locale.CANADA));
oneOf(shoppingCartService).saveOrUpdate(cart); will(returnValue(cart));
oneOf(requestHelper).updateSessionLocale(request, response, Locale.CANADA);
}
});
ModelAndView view = controller.handleRequestInternal(request, response);
assertTrue("View is not redirect view", view.getView() instanceof RedirectView);
assertEquals("Redirect url is not correct.", REFERER_URL, ((AbstractUrlBasedView) view.getView()).getUrl());
}
TargetUrlTaggerTest (MockHttpServletRequest constructor)
The following test demonstrates that the MockHttpServletRequest provides convenience constructors and in this case it accepts the type of request and the request URI. In addition this test sets the query string and address information. The MockHttpServletRequest allows for a large degree of control over the request and can be used for testing any object that requires an HttpServletRequest parameter, not just Spring controllers.
/**
* Tests if the execute method puts the TARGET_URL into the tag set.
*/
@Test
public void testExecutePutsTheTargetURLIntoTagSet() {
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/index.ep");
request.setQueryString("coupon=go");
request.setLocalAddr("demo.elasticpath.com");
request.setLocalPort(Integer.parseInt("8080"));
targetUrlTagger.execute(session, request);
StringBuffer requestURL = request.getRequestURL();
requestURL.append(QUESTION_MARK);
requestURL.append(request.getQueryString());
assertEquals(1, tagSet.getTags().size());
assertEquals(tagSet.getTagValue("TARGET_URL").getValue(), requestURL.toString());
}
StoreSelectionFilterTest (Using MockHttpServletResponse and MockFilterChain)
The testFilterWithNoCandidates method uses the MockFilterChain simply as a means for invoking the filter.doFilter method. The MockFilterChain object merely asserts that the request/response objects are not null. It also makes use of the response mock object to verify that the response status and content types have been set correctly.
@Before
public void setUp() throws Exception {
request = new MockHttpServletRequest();
response = new MockHttpServletResponse();
filterChain = new MockFilterChain();
...
}
/**
* Test that the filter with no selection candidates returns an error in the response.
*/
@Test
public void testFilterWithNoCandidates() {
try {
filter.doFilter(request, response, filterChain);
} catch (IOException e) {
fail(UNEXPECTED_IO_ERROR + e);
} catch (ServletException e) {
fail(UNEXPECTED_SERVLET_EXCEPTION + e);
}
assertEquals("Reponse should return 200 OK", HttpServletResponse.SC_OK, response.getStatus());
assertEquals("Reponse should be empty", 0, response.getContentLength());
}
Other Spring Testing Objects
We have looked at the Mock objects within Spring's org.springframework.mock.web package, but Spring also provides Mock objects for testing JNDI and Portlets. In addition, Spring also provides the ModelAndViewAssert class which provides convenience methods to test model attribute types and values. The methods on this class can be accessed statically or by extending the test class from AbstractModelAndViewTests.
It must also be mentioned that although outside the scope of this document, Spring also provides a wealth of testing classes available for integration testing. This could be the subject of a future article.