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:-
requestComment
is null, which returnsredirect:/dashboard
view.- Form validation contains error, which returns
comment
view. - Everything works fine, which returns
ok
view.
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