Unit-testing Spring MVC: It gets even awesomer - No Fluff Just Stuff

Unit-testing Spring MVC: It gets even awesomer

Posted by: Craig Walls on February 4, 2008

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.
                             DefaultAnnotationHandlerMapping" />
      
  <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.

Craig Walls

About Craig Walls

Craig Walls is a Principal Engineer, Java Champion, Alexa Champion, and the author of Spring AI in Action, Spring in Action, and Build Talking Apps. He's a zealous promoter of the Spring Framework, speaking frequently at local user groups and conferences and writing about Spring. When he's not slinging code, Craig is planning his next trip to Disney World or Disneyland and spending as much time as he can with his wife, two daughters, 1 bird and 2 dogs.

Why Attend the NFJS Tour?

  • » Cutting-Edge Technologies
  • » Agile Practices
  • » Peer Exchange

Current Topics:

  • Languages on the JVM: Scala, Groovy, Clojure
  • Enterprise Java
  • Core Java, Java 8
  • Agility
  • Testing: Geb, Spock, Easyb
  • REST
  • NoSQL: MongoDB, Cassandra
  • Hadoop
  • Spring 4
  • Cloud
  • Automation Tools: Gradle, Git, Jenkins, Sonar
  • HTML5, CSS3, AngularJS, jQuery, Usability
  • Mobile Apps - iPhone and Android
  • More...
Learn More »