Search code examples
spring-mvcmockitomultipartform-datamockmvc

How to resolve MethodArgumentConversionNotSupportedException with MockMvc?


I'm writing a unit test for a controller method that accepts a MultipartFile and a custom object MessageAttachment. So far I can see that the MultipartFile is the correct format for the request but the MessageAttachment is not.

The parsing of the messageAttachment throws a server side 500 error with MethodArgumentConversionNotSupportedException.

It seem to be an issue with converting the MessageAttachment to a MockMultipartFile in the test. This is similar to the example shown here - https://stackoverflow.com/a/21805186

Question:

How can you resolve a MethodArgumentConversionNotSupportedException with MockMvc?

Controller method under test

 @RequestMapping(value = "/", method = RequestMethod.POST, consumes = "multipart/form-data", produces = "application/json")
        public ResponseEntity<MessageAttachment> handleFileUpload(@RequestParam(value = "file", required = true) MultipartFile file, @RequestParam(value = "messageAttachment") MessageAttachment messageAttachment) {

            //do stuff with the file and attachment passed in..  
            MessageAttachment attachment = new MessageAttachment();

            return ResponseEntity.accepted().header(HttpHeaders.CONTENT_DISPOSITION,
                    "attachment; filename=\"" + file.getOriginalFilename() + "\"").body(attachment);
        }

MockMvc Test

@Test
public void shouldSaveUploadedFile() throws Exception {


        // Given
        ObjectMapper mapper = new ObjectMapper();

        MessageAttachment messageAttachment = new MessageAttachment();  
        messageAttachment.setTimestamp(new Date());

        MockMultipartFile multipartFile = new MockMultipartFile("file", "test.txt", "text/plain",
                "Spring Framework".getBytes());

        //Mapping the msgAttachment to a MockMultipartFile HERE
        MockMultipartFile msgAttachment = new MockMultipartFile("messageAttachment", "","application/json",
                 mapper.writeValueAsString(messageAttachment).getBytes());

        // When
        this.mockMvc.perform(MockMvcRequestBuilders.multipart("/media/")
                .file(multipartFile)
                .file(msgAttachment)).andDo(MockMvcResultHandlers.print());

}

Console output of MockMvcResultHandlers.print()

MockHttpServletRequest:                                                                                                                                                                                                                         
      HTTP Method = POST                                                                                                                                                                                                                        
      Request URI = /media/                                                                                                                                                                                                                     
       Parameters = {}                                                                                                                                                                                                                          
          Headers = {Content-Type=[multipart/form-data]}                                                                                                                                                                                        
             Body = <no character encoding set>                                                                                                                                                                                                 
    Session Attrs = {}                                                                                                                                                                                                                          

Handler:                                                                                                                                                                                                                                        
             Type = com.fizz.buzz.fizzapi.controller.MediaUploadController                                                                                                                                                             
           Method = public org.springframework.http.ResponseEntity<com.fizz.buzz.fizzapi.model.MessageAttachment> com.fizz.buzz.fizzapi.controller.MediaUploadController.handleFileUpload(org.springframework.web.multipart.Mu
ltipartFile,com.fizz.buzz.fizzapi.model.MessageAttachment,javax.servlet.http.HttpServletRequest)                                                                                                                                       

Async:                                                                                                                                                                                                                                          
    Async started = false                                                                                                                                                                                                                       
     Async result = null                                                                                                                                                                                                                        

Resolved Exception:                                                                                                                                                                                                                             
             Type = org.springframework.web.method.annotation.MethodArgumentConversionNotSupportedException                                                                                                                                     

ModelAndView:                                                                                                                                                                                                                                   
        View name = null                                                                                                                                                                                                                        
             View = null                                                                                                                                                                                                                        
            Model = null                                                                                                                                                                                                                        

Solution

  • You'll want to use @RequestPart instead of @RequestParam for the part of the request that is application/json. The javadoc for @RequestPart states

    Supported method argument types include MultipartFile in conjunction with Spring's MultipartResolver abstraction, javax.servlet.http.Part in conjunction with Servlet 3.0 multipart requests, or otherwise for any other method argument, the content of the part is passed through an HttpMessageConverter taking into consideration the 'Content-Type' header of the request part. This is analogous to what @RequestBody does to resolve an argument based on the content of a non-multipart regular request.

    Note that @RequestParam annotation can also be used to associate the part of a "multipart/form-data" request with a method argument supporting the same method argument types. The main difference is that when the method argument is not a String, @RequestParam relies on type conversion via a registered Converter or PropertyEditor while @RequestPart relies on HttpMessageConverters taking into consideration the 'Content-Type' header of the request part. @RequestParam is likely to be used with name-value form fields while @RequestPart is likely to be used with parts containing more complex content (e.g. JSON, XML).

    Presumably, you haven't registered a Converter, nor a PropertyEditor, to parse the content of that part, whereas an HttpMessageConverter for JSON is automatically registered (depending on your Spring MVC/Boot version) if you have Jackson on the classpath.