Embracing the Messiness in Search of Epic Solutions

MockMvc + Mockito = Epic Tests

Posted

in

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 returns redirect:/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.

Comments

2 responses to “MockMvc + Mockito = Epic Tests”

  1. scott Avatar
    scott

    Very nice ~ helped me and thank you!

Leave a Reply