Unit-testing Spring MVC: It gets even awesomer
from Spring-Loaded by Craig Walls
http://www.jroller.com/habuma/entry/unit_testing_spring_mvc_it
I just read this mini-article by John Ferguson Smart describing how
wonderfully testable Spring MVC can be. He's absolutely right...Spring
MVC is remarkably testable. While Mr. Smart's article is a good read,
I must let you know that it gets even better.
I've just recently gone through the exercise of updating my RoadRantz
example from Spring in Action, 2E to take advantage of many of the
newest features included in Spring 2.5. One of the most significant
changes that I made was to use annotation-driven Spring MVC which,
aside from greatly reducing the amount of XML configuration required,
makes my Spring MVC controllers even more testable than before.
Consider, for example, this all new version of HomePageController:
package com.roadrantz.mvc;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import com.roadrantz.service.RantService;
@Controller
@RequestMapping("/home.htm")
public class HomePageController {
@RequestMapping(method = RequestMethod.GET)
public String showHomePage(ModelMap model) {
model.addAttribute(rantService.getRecentRants());
return "home";
}
@Autowired
RantService rantService;
}
I've made liberal use of annotations in HomePageController:
* @Controller is one of a handful of stereotype annotations that
indicates that this class is intended to be used as a Spring MVC
controller.
* The @RequestMapping annotation at the class level maps the URL
pattern "/home.htm" to this controller. At the method level, it
indicates that showHomePage() should handle HTTP GET requests.
* @Autowired is applied to the rantService property to indicate
that this property should be autowired (by type) by the Spring
container. Note that I've also left this property with default package
scoping so that my unit tests can inject mock implementations of
RantService even though there isn't a setter method.
Notice that aside from annotations and the use of ModelMap, this
controller is almost Spring-free. Even then, my choice of ModelMap was
simply a choice of convenience--a regular java.util.Map would've been
sufficient:
public String showHomePage(Map model) {
model.put("rantList", rantService.getRecentRants());
return "home";
}
(In case you're unfamiliar with ModelMap, it's a clever extension of
HashMap that automatically generates entry keys based on the type of
the object placed into the map. In this case a list of Rant objects
gets placed into the ModelMap with the name "rantList".)
Anyhow, the key thing here is that HomePageController's showHomePage()
is super-easy to test. Consider the following test class:
package com.roadrantz.mvc;
import static org.junit.Assert.*;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.springframework.ui.ModelMap;
import com.roadrantz.domain.Rant;
public class HomePageControllerTest {
private HomePageController controller;
@Before
public void setup() {
controller = new HomePageController();
controller.rantService = new FakeRantService();
}
@Test
@SuppressWarnings("unchecked")
public void shouldShowHomePageWithRecentRants() {
ModelMap model = new ModelMap();
assertEquals("home", controller.showHomePage(model));
List rants = (List<Rant>) model.get("rantList");
assertNotNull(rants);
assertEquals(3, rants.size());
assertEquals("Rant 1", rants.get(0).getRantText());
assertEquals("Rant 2", rants.get(1).getRantText());
assertEquals("Rant 3", rants.get(2).getRantText());
}
}
Testing showHomePage() is simply a matter of invoking it with a
ModelMap and then asserting that (1) the correct logical view name of
"home" is returned and (2) the expected list of Rant objects are
placed into the ModelMap. (Note that FakeRantService is a simple fake
implementation of RantService that returns a known set of Rants. This
could've just as easily have been mocked out using EasyMock or some
such mock framework.)
Pretty simple, eh? Because HomePageController doesn't have any hint of
an HttpServletRequest or HttpServletResponse, there's no need to use
any use mock implementations of those classes.
Now consider a controller that takes parameters from the URL:
package com.roadrantz.mvc;
import org.joda.time.LocalDate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import com.roadrantz.service.RantService;
@Controller
@RequestMapping("/rantsForDay.htm")
public class RantsForDayController {
@RequestMapping(method = RequestMethod.GET)
public String showRantsForDay(int month, int day, int year,
ModelMap model) {
LocalDate date = new LocalDate(year, month, day);
model.addAttribute(rantService.getRantsForDay(date));
return "dayRants";
}
@Autowired
RantService rantService;
}
As you can see, this isn't dramatically different. The only thing new
here is that the showRantsForDay() method now takes a few extra
arguments. There's nothing that makes it any harder to test. In fact,
because it's so simple, rather than show you
RantsForDayControllerTest, I'll leave it as an "exercise for the
reader".
But even though I won't show you RantsForDayControllerTest, I will
show you what my Spring XML configuration looks like for the MVC
portion:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/
context"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/
beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-2.5.xsd">
<context:component-scan base-package="com.roadrantz.mvc" />
<bean
class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHand lerMapping" /
<bean id="tilesConfigurer"
class="org.springframework.web.servlet.view.tiles.TilesConfigurer"
p:definitions="/WEB-INF/roadrantz-tiles.xml" />
<bean id="viewResolver"
class="org.springframework.web.servlet.view.InternalResourceViewResolver"
p:viewClass="org.springframework.web.servlet.view.tiles.TilesJstlView" /
</beans>
The <context:component-scan> component automagically registers and
autowires everything it finds in the "com.roadrantz.mvc" package that
is annotated with @Controller (among a few other stereotype
annotations), while the DefaultAnnotationHandlerMapping makes sure
that all of the MVC annotations do their job. For a simple web app,
the only thing else I'd need is an InternalResourceViewController. But
RoadRantz is using Tiles, so I also needed a TilesConfigurer to read
the Tiles configuration.
Along with a DispatcherServlet configuration in web.xml, that's all
the XML you need for Spring MVC. Whether I have one controller or a
thousand, these 4 elements are sufficient for most of my Spring MVC
needs. Convention-over-configuration and annotation-based
configuration handle much of the heavy lifting that previously
required pages of XML.
While I'm talking about ease of testing with Spring, I should also
give props to JPA for being remarkably easy to test due to the fact
that EntityManager and Query are both interfaces and can be easily
mocked out for unit-testing DAOs without requiring some elaborate
database fakery. Thanks to the testability of Spring MVC and JPA, I'm
proud to say that RoadRantz has 100% test coverage.
So, you're probably wondering where you can get a copy of the source
code for the all-new, fully-tested, Spring 2.5-savvy RoadRantz
example. Well, I've made tons of progress and want to share it with
you. But I've still got a few loose ends to tie up first.
Specifically, I want to replace all of the security configuration with
new Spring Security 2.0 goodness. Once that's done, I'll post a
download URL here on my blog.
http://www.jroller.com/habuma/entry/unit_testing_spring_mvc_it