Testing Spring MVC Annotated Controllers from JUnit
Let me begin by clarifying that this post is more about testing the correctness of annotations applied to Spring Controllers rather than unit testing the behavior of the controller methods.Annotations on Spring Controllers are commonly tested manually, together with the views. Aside from the usual concerns that come with it, manual testing also involves significant overhead with
having to redeploy binaries and restarting the web server in-between fixes. While "hot-deploy" reduces a lot of these concerns, it still leaves some room for improvement.
AnnotationMethodHandlerAdapter
Starting with version 2.5, Spring MVC comes with a class called AnnotationMethodHandlerAdapter which can be used to programatically invoke an annotated Spring Controller. The handle() method from this class accepts an HttpServletRequest, an HttpServletResponse, and an Object representing the Controller to invoke. It returns an instance of ModelAndView.
We can use AnnotationMethodHandlerAdapter within a JUnit test case to simulate a web container handling a request to the Spring Controller.
@Test public void testController() throws Exception { ModelAndView modelAndView = handlerAdapter.handle(request, response, myController); assertEquals("index", modelAndView.getViewName()); }We can mock HttpServletRequest and HttpServletResponse using popular mocking frameworks such Mockito or use the built-in MockHttpServletRequest and MockHttpServletResponse from the Spring Test module. This example uses the built-in mocked objects from Spring Test.
Using AnnotationMethodHandlerAdapter together with Generics, we can create an abstract class which serves as a parent class to all Spring Controller unit test cases:
@Ignore("abstract test case") public abstract class AbstractControllerTest<T> { private final T controller; private final AnnotationMethodHandlerAdapter handlerAdapter; private MockHttpServletRequest request; private MockHttpServletResponse response; public AbstractControllerTest(T controller) { handlerAdapter = new AnnotationMethodHandlerAdapter(); this.controller = controller; } @Before public void setUp() { response = new MockHttpServletResponse(); request = new MockHttpServletRequest(); } protected T getController() { return controller; } protected AnnotationMethodHandlerAdapter getHandlerAdapter() { return handlerAdapter; } protected MockHttpServletRequest getRequest() { return request; } protected MockHttpServletResponse getResponse() { return response; } protected ModelAndView invokeController() throws Exception { return handlerAdapter.handle(request, response, controller); } }As a parent class, it takes care of the boiler plate code involved in setting-up and invoking the controller.
Example
Let's say we have a controller which returns a view named "index" and populates the current user's username as a model attribute:
@Controller public class MyController { @RequestMapping(value = "/", method = RequestMethod.GET) public String getHomePage(Principal principal, Model model) { if (principal != null) { model.addAttribute("username", principal.getName()); } return "index"; } }To test this, we create a concrete subclass of AbstractControllerTest and assign MyController as the generic type:
@RunWith(BlockJUnit4ClassRunner.class) public class MyControllerTest extends AbstractControllerTest<MyController> { public MyControllerTest() { super(new MyController()); } @Test public void testGetHomePage() throws Exception { getRequest().setMethod("GET"); getRequest().setServletPath("/"); getRequest().setUserPrincipal(new HttpPrincipal("username", "realm")); ModelAndView modelAndView = invokeController(); assertEquals("index", modelAndView.getViewName()); assertNotNull(modelAndView.getModelMap().get("username")); assertEquals("username", modelAndView.getModelMap().get("username")); } }Line 6 creates an instance of MyController and passes it as a parameter to the parent class' constructor.
Lines 11 to 13 sets-up the HTTP request by assigning the request method, request URL, and a fake Principal.
Line 15 invokes the request against the Spring Controller.
Lines 16-18 asserts the correctness of the returned ModelAndView.
The above snippets are available from Google Code.
On my next post, I will explain how to test Spring Controllers returning a JSON response by extending the above classes.
Subscribe to:
Post Comments
(
Atom
)
I know this is a very helpful site but the thing is I am a newbie to MVC spring . Although this site helps to clear my understanding a bit. It is not giving me any idea about how to make this piece of code work.. The code is missing packages that re required.. can you please also add the packages required... it would be better if it can show the fully compilable program instead of just a part from the working version..
ReplyDeleteThanks for the feedback. To be honest, I purposely try to avoid tutorial-like posts because that's not the real purpose of this blog.
ReplyDeleteBut I do understand your concern that's why if appropriate, I normally include a link to a working source code at the end of each post.
Just used this, and it works great... thanks for the sample...
ReplyDeleteReally nice tutorial. Thanks so much :)
ReplyDelete