Integration Testing POSTing an entire object to Spring MVC controller

121,898

Solution 1

One of the main purposes of integration testing with MockMvc is to verify that model objects are correclty populated with form data.

In order to do it you have to pass form data as they're passed from actual form (using .param()). If you use some automatic conversion from NewObject to from data, your test won't cover particular class of possible problems (modifications of NewObject incompatible with actual form).

Solution 2

I had the same question and it turned out the solution was fairly simple, by using JSON marshaller.
Having your controller just change the signature by changing @ModelAttribute("newObject") to @RequestBody. Like this:

@Controller
@RequestMapping(value = "/somewhere/new")
public class SomewhereController {

    @RequestMapping(method = RequestMethod.POST)
    public String post(@RequestBody NewObject newObject) {
        // ...
    }
}

Then in your tests you can simply say:

NewObject newObjectInstance = new NewObject();
// setting fields for the NewObject  

mockMvc.perform(MockMvcRequestBuilders.post(uri)
  .content(asJsonString(newObjectInstance))
  .contentType(MediaType.APPLICATION_JSON)
  .accept(MediaType.APPLICATION_JSON));

Where the asJsonString method is just:

public static String asJsonString(final Object obj) {
    try {
        final ObjectMapper mapper = new ObjectMapper();
        final String jsonContent = mapper.writeValueAsString(obj);
        return jsonContent;
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}  

Solution 3

I believe that I have the simplest answer yet using Spring Boot 1.4, included imports for the test class.:

public class SomeClass {  /// this goes in it's own file
//// fields go here
}

import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
import org.springframework.http.MediaType
import org.springframework.test.context.junit4.SpringRunner
import org.springframework.test.web.servlet.MockMvc

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status

@RunWith(SpringRunner.class)
@WebMvcTest(SomeController.class)
public class ControllerTest {

  @Autowired private MockMvc mvc;
  @Autowired private ObjectMapper mapper;

  private SomeClass someClass;  //this could be Autowired
                                //, initialized in the test method
                                //, or created in setup block
  @Before
  public void setup() {
    someClass = new SomeClass(); 
  }

  @Test
  public void postTest() {
    String json = mapper.writeValueAsString(someClass);
    mvc.perform(post("/someControllerUrl")
       .contentType(MediaType.APPLICATION_JSON)
       .content(json)
       .accept(MediaType.APPLICATION_JSON))
       .andExpect(status().isOk());
  }

}

Solution 4

I think most of these solutions are far too complicated. I assume that in your test controller you have this

 @Autowired
 private ObjectMapper objectMapper;

If its a rest service

@Test
public void test() throws Exception {
   mockMvc.perform(post("/person"))
          .contentType(MediaType.APPLICATION_JSON)
          .content(objectMapper.writeValueAsString(new Person()))
          ...etc
}

For spring mvc using a posted form I came up with this solution. (Not really sure if its a good idea yet)

private MultiValueMap<String, String> toFormParams(Object o, Set<String> excludeFields) throws Exception {
    ObjectReader reader = objectMapper.readerFor(Map.class);
    Map<String, String> map = reader.readValue(objectMapper.writeValueAsString(o));

    MultiValueMap<String, String> multiValueMap = new LinkedMultiValueMap<>();
    map.entrySet().stream()
            .filter(e -> !excludeFields.contains(e.getKey()))
            .forEach(e -> multiValueMap.add(e.getKey(), (e.getValue() == null ? "" : e.getValue())));
    return multiValueMap;
}



@Test
public void test() throws Exception {
  MultiValueMap<String, String> formParams = toFormParams(new Phone(), 
  Set.of("id", "created"));

   mockMvc.perform(post("/person"))
          .contentType(MediaType.APPLICATION_FORM_URLENCODED)
          .params(formParams))
          ...etc
}

The basic idea is to - first convert object to json string to get all the field names easily - convert this json string into a map and dump it into a MultiValueMap that spring expects. Optionally filter out any fields you dont want to include (Or you could just annotate fields with @JsonIgnore to avoid this extra step)

Solution 5

Another way to solve with Reflection, but without marshalling:

I have this abstract helper class:

public abstract class MvcIntegrationTestUtils {

       public static MockHttpServletRequestBuilder postForm(String url,
                 Object modelAttribute, String... propertyPaths) {

              try {
                     MockHttpServletRequestBuilder form = post(url).characterEncoding(
                           "UTF-8").contentType(MediaType.APPLICATION_FORM_URLENCODED);

                     for (String path : propertyPaths) {
                            form.param(path, BeanUtils.getProperty(modelAttribute, path));
                     }

                     return form;

              } catch (Exception e) {
                     throw new RuntimeException(e);
              }
     }
}

You use it like this:

// static import (optional)
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

// in your test method, populate your model attribute object (yes, works with nested properties)
BlogSetup bgs = new BlogSetup();
      bgs.getBlog().setBlogTitle("Test Blog");
      bgs.getUser().setEmail("[email protected]");
    bgs.getUser().setFirstName("Administrator");
      bgs.getUser().setLastName("Localhost");
      bgs.getUser().setPassword("password");

// finally put it together
mockMvc.perform(
            postForm("/blogs/create", bgs, "blog.blogTitle", "user.email",
                    "user.firstName", "user.lastName", "user.password"))
            .andExpect(status().isOk())

I have deduced it is better to be able to mention the property paths when building the form, since I need to vary that in my tests. For example, I might want to check if I get a validation error on a missing input and I'll leave out the property path to simulate the condition. I also find it easier to build my model attributes in a @Before method.

The BeanUtils is from commons-beanutils:

    <dependency>
        <groupId>commons-beanutils</groupId>
        <artifactId>commons-beanutils</artifactId>
        <version>1.8.3</version>
        <scope>test</scope>
    </dependency>
Share:
121,898
Pete
Author by

Pete

Oh, so they have internet on computers now!

Updated on July 15, 2022

Comments

  • Pete
    Pete almost 2 years

    Is there a way to pass an entire form object on mock request when integration testing a spring mvc web app? All I can find is to pass each field separately as a param like this:

    mockMvc.perform(post("/somehwere/new").param("items[0].value","value"));
    

    Which is fine for small forms. But what if my posted object gets larger? Also it makes the test code look nicer if I can just post an entire object.

    Specifically I'd like to test the selection of multiple items by checkbox and then posting them. Of course I could just test posting a single item, but I was wondering..

    We're using spring 3.2.2 with the spring-test-mvc included.

    My Model for the form looks something like this:

    NewObject {
        List<Item> selection;
    }
    

    I've tried calls like this:

    mockMvc.perform(post("/somehwere/new").requestAttr("newObject", newObject) 
    

    to a Controller like this:

    @Controller
    @RequestMapping(value = "/somewhere/new")
    public class SomewhereController {
    
        @RequestMapping(method = RequestMethod.POST)
        public String post(
                @ModelAttribute("newObject") NewObject newObject) {
            // ...
        }
    

    But the object will be empty (yes I've filled it before in the test)

    The only working solution I found was using @SessionAttribute like this: Integration Testing of Spring MVC Applications: Forms

    But I dislike the idea of having to remember to call complete at the end of every controller where I need this. After all the form data does not have to be inside the session, I only need it for the one request.

    So the only thing I can think of right now is to write some Util class that uses the MockHttpServletRequestBuilder to append all the object fields as .param using reflections or individually for each test case..

    I don't know, feeld un-intuitive..

    Any thoughts / ideas on how I might make my like easier? (Apart from just calling the controller directly)

    Thanks!

    • DarthCoder
      DarthCoder almost 11 years
      try using gson and convert the object to json and post it ??
    • Pete
      Pete almost 11 years
      how will that help? My form will post MediaType.APPLICATION_FORM_URLENCODED data so my test should send that data.. I've even tried the convert from the link I postet sending byte[] to the controller but it still won't pick it up..
  • Pete
    Pete almost 11 years
    Yeah, had thoughts along those lines as well.. On the other hand, I'm not actually testing the form itself anyway, I'm just assuming, that the params I pass in the test are actually present in the form, so when I change my model and the test, the form might still have incompatibility issues, so I thought, why even test it..?!
  • tbraun
    tbraun over 8 years
    too bad, maybe Spring should support .content(Object o) call like RestAssured does
  • nyxz
    nyxz over 8 years
    REST-assured looks pretty nice, but I haven't tried it yet. Thanks for mentioning it.
  • Benjamin Slabbert
    Benjamin Slabbert about 7 years
    I found the solution here helpful: stackoverflow.com/questions/36568518/…
  • Siddharth
    Siddharth over 6 years
    incomplete example, where do i get the "post" method from ?
  • nyxz
    nyxz over 6 years
    @Siddharth Thanks for the feedback! It comes from import static org.springframework.test.web.servlet.request.MockMvcRequestB‌​uilders.post;. I updated my answer.
  • nickolay.laptev
    nickolay.laptev almost 5 years
    This question is obviously not about REST endpoint, so you can remove half of your answer. Then we are left with "Not really sure if its a good idea yet" part
  • nickolay.laptev
    nickolay.laptev almost 5 years
    Changing ModelAttribute with RequestBody and usage of JSON for body assumes changes in all clients. These clients use 'application/x-www-form-urlencoded' content type now and not JSON.
  • reversebind
    reversebind almost 5 years
    huh, what are you talking about? of course its related to REST. the whole idea is for testing REST controllers in the context of spring mvc. Fine, I declare its a good idea after thinking about it for 2+ years.
  • nickolay.laptev
    nickolay.laptev almost 5 years
    You write "If its a rest service" in your answer and "of course its related to REST" in your latest comment. Do you think they correspond to each other? I provided enough information to understand what I wrote, moreover the author of the question did the same.