MocMVC giving HttpMessageNotReadableException

2020-07-10 06:48发布

问题:

I'm still learning my way around testing and I'm trying to get a MockMvc test to work for me. It's a simple REST controller that at this point is only doing some authentication using information from json in the post. I've actually implemented the code, so I know it's working because I get back both the correct response with the correct input and the error messages I've put together, both in a json format. My problem is that the test keeps failing with a HttpMessageNotReadableException, even though the actual code works, so I'm assuming I don't have my test set up right. Any help you guys can give would be great.

Here's my controller

@Controller
public class RequestPaymentController {
protected final Log logger = LogFactory.getLog(getClass());
private PaymentService paymentService;
private LoginService loginService;

@Autowired
public void setPaymentService(PaymentService paymentService){
    this.paymentService =  paymentService;
}
@Autowired
public void setLoginService(LoginService loginService){
    this.loginService =  loginService;
}

@RequestMapping(value = "/requestpayment", method = RequestMethod.POST, headers="Accept=application/json")
@ResponseBody
public ResponseEntity<PaymentResult> handleRequestPayment(@RequestBody PaymentRequest paymentRequest, HttpServletRequest request, HttpServletResponse response, BindingResult result) throws Exception{
    ResponseEntity<PaymentResult> responseEntity = null;
    new LoginValidator().validate(paymentRequest, result);
    boolean valid = loginService.isLoginValid(paymentRequest, result);
    if (valid){
      responseEntity = setValidResponse(paymentRequest);
    }else {
        throw new TumsException("exception message");

    }
    return responseEntity;
}


private ResponseEntity<PaymentResult> setValidResponse(PaymentRequest paymentRequest){
    PaymentResult paymentResult = paymentService.getResults(paymentRequest);

    return new ResponseEntity<PaymentResult>(paymentResult, HttpStatus.OK);
}


}

And here's my test code:

public class RequestPaymentControllerTest {

PaymentService mockPaymentService;
RequestPaymentController requestPaymentController;
HttpServletRequest mockHttpServletRequest;
HttpServletResponse mockHttpServletResponse;
PaymentRequest mockPaymentRequest;
BindingResult mockBindingResult;
LoginService mockLoginService;
PaymentResult mockPaymentResult;
MockMvc mockMvc;


@Before
public void setUp() throws Exception {
    mockPaymentService = createMock(PaymentService.class);
    mockHttpServletRequest = createMock(HttpServletRequest.class);
    mockHttpServletResponse = createMock(HttpServletResponse.class);
    mockPaymentRequest = createMock(PaymentRequest.class);
    requestPaymentController = new RequestPaymentController();
    mockBindingResult = createMock(BindingResult.class);
    mockLoginService = createMock(LoginService.class);
    requestPaymentController.setPaymentService(mockPaymentService);
    mockPaymentResult = createMock(PaymentResult.class);
    mockMvc = MockMvcBuilders.standaloneSetup(new RequestPaymentController()).build();

}

@After
public void tearDown() throws Exception {
    mockPaymentService = null;
    mockHttpServletRequest = null;
    mockHttpServletResponse = null;
    mockPaymentRequest = null;
    requestPaymentController = null;
    mockBindingResult = null;
    mockLoginService = null;
    mockPaymentResult = null;
    mockMvc = null;
}


@Test
public void testHandleRequestPayment() throws Exception{
    initializeStateForHandleRequestPayment();
    createExpectationsForHandleRequestPayment();
    replayAndVerifyExpectationsForHandleRequestPayment();

}



private void initializeStateForHandleRequestPayment(){

}

private void createExpectationsForHandleRequestPayment(){
    mockPaymentRequest.getServiceUsername();
    expectLastCall().andReturn("testuser");
    mockPaymentRequest.getServicePassword();
    expectLastCall().andReturn("password1!");
    mockLoginService.isLoginValid(mockPaymentRequest,mockBindingResult);
    expectLastCall().andReturn(true);
    mockPaymentService.getResults(mockPaymentRequest);
    expectLastCall().andReturn(mockPaymentResult);
}

private void replayAndVerifyExpectationsForHandleRequestPayment() throws Exception{
    replay(mockPaymentService, mockBindingResult, mockHttpServletRequest, mockHttpServletResponse, mockPaymentRequest, mockLoginService);
    requestPaymentController.setLoginService(mockLoginService);
    requestPaymentController.handleRequestPayment(mockPaymentRequest, mockHttpServletRequest, mockHttpServletResponse, mockBindingResult);
    mockMvc.perform(post("/requestpayment")
            .contentType(MediaType.APPLICATION_JSON)
            .accept(MediaType.APPLICATION_JSON))
            .andDo(print())
            .andExpect(status().isBadRequest());
    verify(mockPaymentService, mockBindingResult, mockHttpServletRequest, mockHttpServletResponse, mockPaymentRequest, mockLoginService);

}
}

The results of the andDo(print()) are:

MockHttpServletRequest:
     HTTP Method = POST
     Request URI = /requestpayment
      Parameters = {}
         Headers = {Content-Type=[application/json], Accept=[application/json]}

         Handler:
            Type = portal.echecks.controller.RequestPaymentController
          Method = public org.springframework.http.ResponseEntity<portal.echecks.model.PaymentResult> portal.echecks.controller.RequestPaymentController.handleRequestPayment(portal.echecks.model.PaymentRequest,javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse,org.springframework.validation.BindingResult) throws java.lang.Exception

  Resolved Exception:
            Type = org.springframework.http.converter.HttpMessageNotReadableException

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

        FlashMap:

MockHttpServletResponse:
          Status = 400
   Error message = null
         Headers = {}
    Content type = null
            Body = 
   Forwarded URL = null
  Redirected URL = null
         Cookies = []

Process finished with exit code 0

As you can see, the test passes when I'm expecting a bad request status, but I've put in logging and I know that the ResponseBody I'm sending back has a 200 status. Like I said, this is my first time with MockMvc, so I assume I've not set something up right. Any suggestions?

回答1:

An HttpMessageNotReadableException is

Thrown by HttpMessageConverter implementations when the read method fails.

You also get a 400 Bad Request in your response. This should all tell you that you are not sending what your server is expecting. What is your server expecting?

@RequestMapping(value = "/requestpayment", method = RequestMethod.POST, headers="Accept=application/json")
@ResponseBody
public ResponseEntity<PaymentResult> handleRequestPayment(@RequestBody PaymentRequest paymentRequest, HttpServletRequest request, HttpServletResponse response, BindingResult result) throws Exception{

The main thing here is the @RequestBody annotated parameter. So you are telling your server to try and deserialize a PaymentRequest instance from the body of the HTTP POST request.

So let's see the request you are making

mockMvc.perform(post("/requestpayment")
        .contentType(MediaType.APPLICATION_JSON)
        .accept(MediaType.APPLICATION_JSON))
        .andDo(print())
        .andExpect(status().isBadRequest());

I don't see you providing a body to the request. There should be a content(String) call somewhere in there to set the content of the POST request. This content should be a JSON serialization of a PaymentRequest.

Note that because you are using the StandaloneMockMvcBuilder, you might need to set the HttpMessageConverter instances yourself, ie. a MappingJackson2HttpMessageConverter to serialize and deserialize JSON.


Note that the BindingResult parameter should come immediately after the parameter to which it's related. Like so

@RequestMapping(value = "/requestpayment", method = RequestMethod.POST, headers="Accept=application/json")
@ResponseBody
public ResponseEntity<PaymentResult> handleRequestPayment(@Valid @RequestBody PaymentRequest paymentRequest, BindingResult result, HttpServletRequest request, HttpServletResponse response) throws Exception{

Don't forget the @Valid.

Note that this

requestPaymentController.setLoginService(mockLoginService);
requestPaymentController.handleRequestPayment(mockPaymentRequest, mockHttpServletRequest, mockHttpServletResponse, mockBindingResult);

is completely unrelated to the MockMvc test you are doing.



回答2:

In my case, as sprint mvc w/ jackson (jackson-mapper-asl, v-1.9.10) deserialization requires JSON parser. And jackson requires a default constructor for http request message deserialization, if there's no default constructor, jackson will have a problem w/ reflection and throws HttpMessageNotReadableException exception.

This is to say, all the classes/sub-classes which used as Request body, (in this case) requires a default constructor. This costed me a few moments after I tried adding custom converter and other suggestions I got in stackoverflow in vain.

Or you can add Custom Deserializer or Mixin annotation to avoid adding default constructor hierachically everywhere. as described here: http://blogs.jbisht.com/blogs/2016/09/12/Deserialize-json-with-Java-parameterized-constructor. Check this if you're interested.


Seems duplicated here > Spring HttpMessageNotReadableException.



回答3:

Make sure of the following:

  • return object implements Serializable
  • @ResponseBody annotation used on the controller method
  • On your unit test

    @ExtendWith(SpringExtension.class) @ContextConfiguration(classes = {....}) @WebMvcTest @AutoConfigureMockMvc



回答4:

Probably too late to answer but just in case someone is still looking at this page.

As @Sotirios Delimanolis mentions, the problem is due to a bad request - a '@RequestBody' is specified in the parameter but never supplied in the request body. So, if you add that to request using 'content(someRequestString)' as below, it should work.

PaymentRequest  paymentRequest  = new PaymentRequest(...);
String requestBody = new ObjectMapper().valueToTree(paymentRequest).toString();
mockMvc.perform(post("/requestpayment")
                .content(requestBody)
                .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.status").value("SUCCESS"))
                .andExpect(jsonPath("$.paymentAmount", is(20)));

jsonPath may be used to verify the attributes on the response. In the above example, say PaymentResponse has attributes status and paymentAmount in the json response. These parts can be verified easily.

You may run into errors like -

NoClassDefFoundError: com/jayway/jsonpath/Predicate

while using jsonPath. So, make sure it is added to classpath explicitly as it is an optional dependency in spring-test and will not be available transitively. If using maven, do this:

<dependency>
  <groupId>com.jayway.jsonpath</groupId>
  <artifactId>json-path</artifactId>
  <version>2.4.0</version>
  <scope>test</scope>      
</dependency>