Spring integration testing for controllers

Previously we discovered the basics of integration testing in Spring. So now it's a great time to deepen this subject.

In this article we'll focus more preciselly on web-oriented Spring integration testing. In the first part we'll discover the elements that can be tested. The second part will be more practice and we'll approach the classes and methods to use in writing tests for Springs's web application. At the last, third part, we'll write some tests to cover exposed cases from the first part.

Test cases in Spring integration testing

In the article about introduction to integration tests in Spring, we saw some tests for services layer. But it's not the only part that we can test in web version of the framework. We can also test controllers behavior by using Spring-testing mocked objects. Thanks to them, we can check:
- returned view: we can verify if the incorrect request (containing for example invalid model) returns the view corresponded to validation error.
- accepted headers: we can check if the correct method is invoked for given URL. For example, we can check if writing method doesn't accept data transmitted in GET.
- flash data: thanks to testing classes, we can simply check if some of expected flash attributes are present during request processing.
- response state: if returned response is in the expected state (for example: 200 response code or a word present in the response's body).
- authorization: we can be sure that a user can't make some unauthorized actions (for example: anonymous user can't add a new comment).
- file upload: every controllers can be checked, even those that must handle uploaded files.

To do all those things, Spring mocks real-world objects into specific testing ones. A mocking objects imitate behavior of real objects in controlled way. They are used when the real objects are not adapted to testing procedures. Imagine below situation: you have an heavy object initialized in 1 minute. This is caused by some of complex and dynamic operations. But in the case of unit or integration tests, this heavy object doesn't need to be initialized with dynamic operations. It can simply have static and fixed values. To accelerate the tests, we could make a mocked object corresponding to the real object. This mocked object will make exactly the same things. Only difference is that it won't be based on dynamic initialization and its initialization time will be divided by 10.

Mocking classes in Spring

Spring uses mocking to simulate MVC behaviour. We can retreive some of explicit mocked objects, as: org.springframework.mock.web.MockHttpServletRequest and org.springframework.mock.web.MockHttpServletResponse, representing respectively, HttpServletRequest and HttpServletResponse implementations from javax.servlet.http package. Another interesting mocked objects can be retreived in following list:

One more, and very important class, is org.springframework.test.web.servlet.MockMvc. It's a kind of main MVC tests executor. Its method public ResultActions perform(RequestBuilder requestBuilder) throws Exception executes HTTP request basing on provided requestBuilder parameter. RequestBuilder's objects automates test context construction: they create mocked HTTP request, apply defined filters and, finally, return the instances of MvcResult implementations. These implementations return every objects used to create mocked HTTP response, such as: interceptors, handlers or mocked HTTP request. They permit also an access to flash attributes or exceptions caused by the test. All classes and interfaces are located in the same package, org.springframework.test.web.servlet.

Spring integration testing in practice

After learning the new information about integration testing in Spring, we can start to code one more advanced test case. This time we'll check the elements defined in the first part: returned view, accepted headers, flash data and response state:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations={"classpath:applicationContext-test.xml"})
@WebAppConfiguration
public class RequestTest {
  @Autowired
  private WebApplicationContext wac;

  private MockMvc mockMvc;

  @Before
  public void setup() {
    this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
  }

  @Test
  public void productAddFailure() {
    try {
      MvcResult result = this.mockMvc.perform(post("/products/add"))
      .andDo(print())
      .andExpect(status().is(302))
      .andReturn();
      assertTrue("Returned map doesn't contain message entry", result.getFlashMap().get("message") == "An error occurred");
      assertTrue("Error should be present but is not", result.getFlashMap().get("errors") != null);
    } catch (Exception e) {
      e.printStackTrace();
      fail("Test failed because of :"+e.getMessage());
    }
  }

  @Test
  public void productAddSuccess() {
    try {
      this.mockMvc.perform(post("/products/add")
      .param("name", "test product#2")
      )
      .andDo(print())
      .andExpect(status().is(200))
      .andExpect(model().errorCount(0))
      .andExpect(model().hasNoErrors())
      .andExpect(view().name("success"))
      .andReturn();
    } catch (Exception e) {
      e.printStackTrace();
      fail("Test failed because of :"+e.getMessage());
    }
  }
}

Let's analyze the tests one by one. The first, productAddFailure, checks if product adding doesn't work without correct parameters. In our case, the field name must to be filled. MockMvc instance performs (perform()) an POST request. We are expecting (andExpect()) that the response status will be 302 (status().is(302)). In additionnaly, we call an action in andDo() which prints all information about this test:

MockHttpServletRequest:
  HTTP Method = POST
  Request URI = /products/add
  Parameters = {id=[33]}
  Headers = {}

  Handler:
    Type = com.mysite.controller.TestController
    Method = public java.lang.String com.mysite.controller.TestController.insertProduct
      (com.mysite.data.Product,org.springframework.validation.BindingResult,
      org.springframework.ui.Model,org.springframework.web.servlet.mvc.support.RedirectAttributes)

  Async:
    Was async started = false
    Async result = null

  Resolved Exception:
    Type = null

  ModelAndView:
    View name = redirect:/products/add
    View = null
    Model = null

  FlashMap:
    Attribute = message
    value = An error occurred
    Attribute = errors
    value = org.springframework.validation.BeanPropertyBindingResult: 1 errors
    Field error in object 'product' on field 'name': rejected value [null]; codes 
      [NotEmpty.product.name,NotEmpty.name,NotEmpty.java.lang.String,NotEmpty]; 
      arguments [org.springframework.context.support.DefaultMessageSourceResolvable: 
      codes [product.name,name]; arguments []; default message [name]]; default 
      message [may not be empty]

  MockHttpServletResponse:
    Status = 302
    Error message = null
    Headers = {Location=[/products/add]}
    Content type = null
    Body = 
    Forwarded URL = null
    Redirected URL = /products/add
    Cookies = []

The last used method, andReturn(), returns an MvcResult instance. This object provides an access to every element from treated request. For example, we can access simply to flash attributes send to redirected page. By the way, we use this method to check if validated object is in error state. Beware ! This validation can't be done with .andExpect(model().attributeHasFieldErrors("product", "name")) or .andExpect(model().errorCount(1)). Both of them will return an exception:

java.lang.AssertionError: No BindingResult for attribute: product
  at org.springframework.test.util.AssertionErrors.fail(AssertionErrors.java:39)
  at org.springframework.test.util.AssertionErrors.assertTrue(AssertionErrors.java:72)
  at org.springframework.test.web.servlet.result.ModelResultMatchers.getBindingResult(ModelResultMatchers.java:241)
  at org.springframework.test.web.servlet.result.ModelResultMatchers.access$100(ModelResultMatchers.java:38)
  at org.springframework.test.web.servlet.result.ModelResultMatchers$8.match(ModelResultMatchers.java:160)

This exception is caused because the view returned by tested controller is a redirect: "redirect:/products/add". You could use previously mentioned validations only for a view not being a redirect.

Take a look on the 2nd method which, as its name indicates, must insert new product into database. It uses the same principle as previous method, the principle of performing an action and checking expecting results. Here, because of redirect absence, we can check if validated model has 0 errors (model().errorCount(0) or model().hasNoErrors()). The view name veryfication is made through view().name("success").

Today, we saw some of more advanced testing features of Spring test project. The first part of the article talked about possible test cases. The next part presented the main test classes. At the end, we shown how to write a test using mocked objects.


If you liked it, you should read:

📚 Newsletter Get new posts, recommended reading and other exclusive information every week. SPAM free - no 3rd party ads, only the information about waitingforcode!