Integration Testing Spring MVC Controllers

MockMVC Example

In an earlier post we looked at using the Spring MVC Test project to unit test Spring MVC controllers. That example used Mockito to mock the service layer to ensure isolated testing of the controller. In this post I will focus on the integration side of testing. This will still use Spring MVC Test with MockMVC but will also load the WebApplicationContext to give full access to Spring beans. The cool part about this is that it uses a MockMVC container and your real classes to give the most integrated test you can do before using a browser tool like Selenium or Geb.

Getting Ready

Application Setup
These tests are built on the Spring Data JPA example from a previous post.

For this test we added a few dependencies to the pom.xml

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>3.2.3.RELEASE</version>
</dependency>

<!-- Optional -->
<dependency>
    <groupId>org.hamcrest</groupId>
    <artifactId>hamcrest-core</artifactId>
    <version>1.3</version>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>org.hamcrest</groupId>
    <artifactId>hamcrest-library</artifactId>
    <version>1.3</version>
    <scope>test</scope>
</dependency>

Reference Class

Below is the Index Controller class we are testing:

@Controller
public class IndexController {

    public static final String PAGE_INDEX = "index";
    public static final String PAGE_SHOW = "show";

    @Autowired
    SampleService sampleService;


    @RequestMapping(value = "/", method = RequestMethod.GET)
    public ModelAndView index() {
        return new ModelAndView(PAGE_INDEX, "signupForm", new SignupForm());
    }

    @RequestMapping(value = "/create", method = RequestMethod.POST)
    public String create(Model model, @Valid SignupForm signupForm, BindingResult result) {

        String returnPage = PAGE_INDEX;

        if (!result.hasErrors()) {
            try {
                model.addAttribute("signupForm", sampleService.saveFrom(signupForm));
                returnPage = PAGE_SHOW;
            } catch (InvalidUserException e) {
                model.addAttribute("page_error", e.getMessage());
            }
        }
        return returnPage;
    }

    @RequestMapping(value = "/security-error", method = RequestMethod.GET)
    public String securityError(RedirectAttributes redirectAttributes) {
        redirectAttributes.addFlashAttribute("page_error", "You do have have permission to do that!");
        return "redirect:/";
    }
}

Create the test class with Spring Test MVC

We start by creating a test class for the index controller.

@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration({"classpath:applicationContext.xml", "classpath:applicationContext-dao.xml"})
public class IndexControllerIntegrationTest {

    @Autowired
    private WebApplicationContext wac;

    private MockMvc mockMvc;

    @Before
    public void setup() {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
    }
  • @RunWith(SpringJUnit4ClassRunner.class) tells JUnit to invoke the Spring test wrapper which allows the Web App Context to be loaded. By default Spring will load the context into a Static variable so it only gets initialized once per test run saving a lot of time. This his helpful if you create a base test class with the context info and reuse it across your project.
  • @WebAppConfiguration Lets the Spring wrapper know that we want a WebApplicationContext loaded for the project. This is needed for the Mock MVC setup.
  • @ContextConfiguration({“classpath:applicationContext.xml”, “classpath:applicationContext-dao.xml”}) Is the magic. It defines which Spring definitions we want to load for the test Context. If you need special beans for testing then you can either override the prod files for just specify special test only context definitions here.

    Java Config Option
    If you are using the Java Config style for Spring beans, then you can specify the java config class with the following option:
    @ContextConfiguration(classes = appConfig.class)
  • The class has the WebApplicationContext (wac) Autowired and is then used to construct the MockMvc in the JUnit setup before each test. The WAC has all our normal application beans (service & dao) but not any controllers since they get loaded through the servlet. To get the controllers into the context you can either specific the sample-servlet.xml config in the @ContextConfiguration or add <mvc:annotation-driven/> to a test applicationContext.xml.

    Now that everything is setup we can start writing some tests!

      @Test
      public void testGetSignupForm() throws Exception {
          this.mockMvc.perform(get("/"))
                  .andExpect(status().isOk())
                  .andExpect(forwardedUrl(IndexController.PAGE_INDEX))
                  .andExpect(model().attribute("signupForm", any(SignupForm.class)));
      }
    
      @Test
      public void testCreateSignupFormErrors() throws Exception {
    
        this.mockMvc.perform(post("/create")
                  .param("email", "<error>")
                  .param("firstName", "<error>")
                  .param("lastName", "<error>"))
                  .andExpect(status().isOk())
                  .andExpect(forwardedUrl(IndexController.PAGE_INDEX))
                  .andExpect(model().attributeHasFieldErrors("signupForm", "email"));
      }
    

    This first test does a simple get on the index page. MockMvc will match the get path to those defined in the controller RequestMappings. Once the get request is performed we get access to a hefty response object. There are a lot of nice static methods that will interact with the response. First we check for a 200 status, then the returned page (in this case just a string containing “index”) and finally attributes added to the model. .andExpect(model().attribute(“signupForm”, any(SignupForm.class))); uses a hamcrest matcher to validate that the attribute “signupForm” is at least the correct type of object. If needed we could go a step further and verify the object properties but this is enough in this case.

    The second test does a full form post. We tell the MockMvc to perform a post on “/create” with some params. In this test we want an error so there is just some bad param values. We still expect a 200 status and to be sent to the index page. The last “addExpect” looks at the model for field errors. This makes sure that our form validator fired and found the invalid email.

    Next we’ll add one more test to show a few other test options.

      @Test
      public void testSecurityError() throws Exception {
          this.mockMvc.perform(get("/security-error"))
                .andExpect(status().isFound())
                .andExpect(redirectedUrl("/"))
                .andExpect(flash().attributeExists("page_error"));
      }
    

    In this test we wanted to test a security error. The HDIV security setup is described in another post but the idea is that if there is a XSS or SQL injection attack it will redirect a to “/security-error”. The expectation is that the status is a 302 redirect with a message in an attribute called “page_error”. We are also able to validate which URL we were redirected to using the “redirectedUrl” static method.

    From this you can see that it’s very easy to setup quick integration tests against a Spring MVC controller without the use of selenium. However I really like Geb/Selenium and will talk about that in the next post.

    Testing Series
    This post is one in a series about end to end testing of Spring MVC applications. If you are interested in other levels of application testing then check out the series index here.

One Response

  1. ram March 27, 2015

Leave a Reply

Your email address will not be published. Required fields are marked *