Spring Framework 3.2 introduces a very elegant way to test Spring MVC controller using MockMvc.
Based on the documentation, there are two ways to configure MockMvc:-
MockMvcBuilders.webAppContextSetup(webApplicationContext).build()MockMvcBuilders.standaloneSetup(controller).build()
The first approach will automatically load the Spring configuration and inject WebApplicationContext into the test. The second approach does not load the Spring configuration.
While both options work, my preference is to use the second approach that doesn’t load the Spring configuration. Rather, I use Mockito to mock out all the dependencies within the controller.
EXAMPLE
Let’s assume we want to test this controller:-
@Controller
@RequestMapping(value = "/comment/{uuid}")
public class CommentController {
@Autowired
private RequestService requestService;
@Autowired
private CommentValidator validator;
@InitBinder("commentForm")
protected void initBinder(WebDataBinder binder) {
binder.setValidator(validator);
}
@RequestMapping(method = RequestMethod.POST)
public String saveComment(@PathVariable String uuid,
@Valid @ModelAttribute CommentForm commentForm,
BindingResult result,
Model model) {
RequestComment requestComment = requestService.getRequestCommentByUUID(uuid);
if (requestComment == null) {
return "redirect:/dashboard";
}
if (result.hasErrors()) {
return "comment";
}
return "ok";
}
}
SETTING UP TEST FILE
To test /comment/{uuid} POST, we need three tests:-
requestCommentis null, which returnsredirect:/dashboardview.- Form validation contains error, which returns
commentview. - Everything works fine, which returns
okview.
The test file looks something like this:-
public class CommentControllerTest {
@Mock
private RequestService requestService;
@Mock
private CommentValidator validator;
@InjectMocks
private CommentController commentController;
private MockMvc mockMvc;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
mockMvc = MockMvcBuilders.standaloneSetup(commentController).build();
when(validator.supports(any(Class.class))).thenReturn(true);
}
@Test
public void testSaveComment_RequestCommentNotFound() throws Exception {
...
}
@Test
public void testSaveComment_FormError() throws Exception {
...
}
@Test
public void testSaveComment_NoError() throws Exception {
...
}
}
Because the controller has two dependencies ( RequestService and CommentValidator ) injected into it through Spring autowiring, we are going to create these two mocks and inject them into the controller by annotating them with Mockito’s @Mock and @InjectMocks accordingly.
In setup(...) method, the first line initializes objects annotated with @Mock. The second line initializes MockMvc without loading Spring configuration because we want the flexibility to mock the dependencies out using Mockito. The third line instructs the validator to return true when validator.support(...) is invoked. If the third line is left out, you will get this exception when binder.setValidator(validator) in the controller.initBinder(...) is invoked:-
org.springframework.web.util.NestedServletException: Request processing failed;
nested exception is java.lang.IllegalStateException: Invalid target for Validator
[validator]: com.choonchernlim.epicapp.form.CommentForm@1a80b973
Finally, we have three stubs to test the conditions defined earlier.
TEST CASE 1: requestComment is null
public class CommentControllerTest {
...
@Test
public void testSaveComment_RequestCommentNotFound() throws Exception {
when(requestService.getRequestCommentByUUID("123")).thenReturn(null);
mockMvc.perform(post("/comment/{uuid}", "123"))
.andExpect(status().isMovedTemporarily())
.andExpect(view().name("redirect:/dashboard"));
}
}
The first test is rather easy. All we need to do is to instruct requestService.getRequestCommentByUUID(...) to return null when it is invoked.
TEST CASE 2: Form validation contains error
public class CommentControllerTest {
...
@Test
public void testSaveComment_FormError() throws Exception {
when(requestService.getRequestCommentByUUID("123")).thenReturn(new RequestComment());
doAnswer(new Answer() {
@Override
public Object answer(InvocationOnMock invocationOnMock) throws Throwable {
Errors errors = (Errors) invocationOnMock.getArguments()[1];
errors.reject("forcing some error");
return null;
}
}).when(validator).validate(anyObject(), any(Errors.class));
mockMvc.perform(post("/comment/{uuid}", "123"))
.andExpect(status().isOk())
.andExpect(view().name("comment"));
}
}
The second test is a little complicated. We need to instruct requestService.getRequestCommentByUUID(...) to return a RequestComment object. Then, we use Mockito’s doAnswer(...) to set a dummy error value to Errors object, which is the second argument of validator.validate(...). Setting this value will cause result.hasErrors() to evaluate to true.
TEST CASE 3: Everything works fine
public class CommentControllerTest {
...
@Test
public void testSaveComment_NoError() throws Exception {
when(requestService.getRequestCommentByUUID("123")).thenReturn(new RequestComment());
mockMvc.perform(post("/comment/{uuid}", "123"))
.andExpect(status().isOk())
.andExpect(view().name("ok"));
}
}
The third test is rather easy too. We need to instruct requestService.getRequestCommentByUUID(...) to return a RequestComment object… and that’s it.
CONCLUSION
See… it’s not really that hard. The combination of Mockito and Spring Test Framework 3.2 provides us a great flexibility to test our Spring MVC controllers. Granted, we should already have our unit tests for the two dependencies ( RequestService and CommentValidator ) already. So, it is safe to alter the behavior of these dependencies using Mockito to test every possible path in the controller.
Leave a Reply