[java] Integration Testing POSTing an entire object to Spring MVC controller

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!

This question is related to java spring testing spring-mvc integration-testing

The answer is


Here is the method I made to transform recursively the fields of an object in a map ready to be used with a MockHttpServletRequestBuilder

public static void objectToPostParams(final String key, final Object value, final Map<String, String> map) throws IllegalAccessException {
    if ((value instanceof Number) || (value instanceof Enum) || (value instanceof String)) {
        map.put(key, value.toString());
    } else if (value instanceof Date) {
        map.put(key, new SimpleDateFormat("yyyy-MM-dd HH:mm").format((Date) value));
    } else if (value instanceof GenericDTO) {
        final Map<String, Object> fieldsMap = ReflectionUtils.getFieldsMap((GenericDTO) value);
        for (final Entry<String, Object> entry : fieldsMap.entrySet()) {
            final StringBuilder sb = new StringBuilder();
            if (!GenericValidator.isEmpty(key)) {
                sb.append(key).append('.');
            }
            sb.append(entry.getKey());
            objectToPostParams(sb.toString(), entry.getValue(), map);
        }
    } else if (value instanceof List) {
        for (int i = 0; i < ((List) value).size(); i++) {
            objectToPostParams(key + '[' + i + ']', ((List) value).get(i), map);
        }
    }
}

GenericDTO is a simple class extending Serializable

public interface GenericDTO extends Serializable {}

and here is the ReflectionUtils class

public final class ReflectionUtils {
    public static List<Field> getAllFields(final List<Field> fields, final Class<?> type) {
        if (type.getSuperclass() != null) {
            getAllFields(fields, type.getSuperclass());
        }
        // if a field is overwritten in the child class, the one in the parent is removed
        fields.addAll(Arrays.asList(type.getDeclaredFields()).stream().map(field -> {
            final Iterator<Field> iterator = fields.iterator();
            while(iterator.hasNext()){
                final Field fieldTmp = iterator.next();
                if (fieldTmp.getName().equals(field.getName())) {
                    iterator.remove();
                    break;
                }
            }
            return field;
        }).collect(Collectors.toList()));
        return fields;
    }

    public static Map<String, Object> getFieldsMap(final GenericDTO genericDTO) throws IllegalAccessException {
        final Map<String, Object> map = new HashMap<>();
        final List<Field> fields = new ArrayList<>();
        getAllFields(fields, genericDTO.getClass());
        for (final Field field : fields) {
            final boolean isFieldAccessible = field.isAccessible();
            field.setAccessible(true);
            map.put(field.getName(), field.get(genericDTO));
            field.setAccessible(isFieldAccessible);
        }
        return map;
    }
}

You can use it like

final MockHttpServletRequestBuilder post = post("/");
final Map<String, String> map = new TreeMap<>();
objectToPostParams("", genericDTO, map);
for (final Entry<String, String> entry : map.entrySet()) {
    post.param(entry.getKey(), entry.getValue());
}

I didn't tested it extensively, but it seems to work.


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>

I ran into the same issue a while ago and did solve it by using reflection with some help from Jackson.

First populate a map with all the fields on an Object. Then add those map entries as parameters to the MockHttpServletRequestBuilder.

In this way you can use any Object and you are passing it as request parameters. I'm sure there are other solutions out there but this one worked for us:

    @Test
    public void testFormEdit() throws Exception {
        getMockMvc()
                .perform(
                        addFormParameters(post(servletPath + tableRootUrl + "/" + POST_FORM_EDIT_URL).servletPath(servletPath)
                                .param("entityID", entityId), validEntity)).andDo(print()).andExpect(status().isOk())
                .andExpect(content().contentType(MediaType.APPLICATION_JSON)).andExpect(content().string(equalTo(entityId)));
    }

    private MockHttpServletRequestBuilder addFormParameters(MockHttpServletRequestBuilder builder, Object object)
            throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {

        SimpleDateFormat dateFormat = new SimpleDateFormat(applicationSettings.getApplicationDateFormat());

        Map<String, ?> propertyValues = getPropertyValues(object, dateFormat);

        for (Entry<String, ?> entry : propertyValues.entrySet()) {
            builder.param(entry.getKey(),
                    Util.prepareDisplayValue(entry.getValue(), applicationSettings.getApplicationDateFormat()));
        }

        return builder;
    }

    private Map<String, ?> getPropertyValues(Object object, DateFormat dateFormat) {
        ObjectMapper mapper = new ObjectMapper();
        mapper.setDateFormat(dateFormat);
        mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        mapper.registerModule(new JodaModule());

        TypeReference<HashMap<String, ?>> typeRef = new TypeReference<HashMap<String, ?>>() {};

        Map<String, ?> returnValues = mapper.convertValue(object, typeRef);

        return returnValues;

    }

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)


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());
  }

}

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);
    }
}  

Examples related to java

Under what circumstances can I call findViewById with an Options Menu / Action Bar item? How much should a function trust another function How to implement a simple scenario the OO way Two constructors How do I get some variable from another class in Java? this in equals method How to split a string in two and store it in a field How to do perspective fixing? String index out of range: 4 My eclipse won't open, i download the bundle pack it keeps saying error log

Examples related to spring

Are all Spring Framework Java Configuration injection examples buggy? Two Page Login with Spring Security 3.2.x Access blocked by CORS policy: Response to preflight request doesn't pass access control check Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured ApplicationContextException: Unable to start ServletWebServerApplicationContext due to missing ServletWebServerFactory bean Failed to auto-configure a DataSource: 'spring.datasource.url' is not specified Spring Data JPA findOne() change to Optional how to use this? After Spring Boot 2.0 migration: jdbcUrl is required with driverClassName The type WebMvcConfigurerAdapter is deprecated No converter found capable of converting from type to type

Examples related to testing

Test process.env with Jest How to configure "Shorten command line" method for whole project in IntelliJ Jest spyOn function called Simulate a button click in Jest Mockito - NullpointerException when stubbing Method toBe(true) vs toBeTruthy() vs toBeTrue() How-to turn off all SSL checks for postman for a specific site What is the difference between smoke testing and sanity testing? ReferenceError: describe is not defined NodeJs How to properly assert that an exception gets raised in pytest?

Examples related to spring-mvc

Two Page Login with Spring Security 3.2.x ApplicationContextException: Unable to start ServletWebServerApplicationContext due to missing ServletWebServerFactory bean Spring 5.0.3 RequestRejectedException: The request was rejected because the URL was not normalized The type WebMvcConfigurerAdapter is deprecated RestClientException: Could not extract response. no suitable HttpMessageConverter found Spring boot: Unable to start embedded Tomcat servlet container UnsatisfiedDependencyException: Error creating bean with name 8080 port already taken issue when trying to redeploy project from Spring Tool Suite IDE Error creating bean with name 'entityManagerFactory' defined in class path resource : Invocation of init method failed Difference between the annotations @GetMapping and @RequestMapping(method = RequestMethod.GET)

Examples related to integration-testing

Testing Spring's @RequestBody using Spring MockMVC Integration Testing POSTing an entire object to Spring MVC controller How do I add a new sourceset to Gradle? What's the difference between unit tests and integration tests? Before and After Suite execution hook in jUnit 4.x