Validating if request body in HTTP POST request is null in Spring Boot controller - spring

I am replacing manual validation of input to a POST request in a Spring Boot REST-controller. JSR-303 Spring Bean Validation is used for validating the instance variables in the request body and this is working as expected. What is the recommended method to validate that the object in the request body is not null?
I have tried:
annotating the entire object such as this: #NotNull #Valid #RequestBody Foo foo
annotating the entire class with #NotNull
I am replacing:
#PostMapping...
public ResponseEntity<Map<String, Object>> editFoo(
#RequestBody Foo foo, ...) {
if(foo == null) {
return (new ResponseEntity<>(headers, HttpStatus.BAD_REQUEST));
}
}
with a Bean Validation equivalent:
#PostMapping...
public ResponseEntity<Map<String, Object>> editFoo(
#Valid #RequestBody Foo foo, ...) {
...
}
I tried unit testing the controller method by:
// Arrange
Foo foo = null;
String requestBody = objectMapper.writeValueAsString(foo);
// Act + assert
mockMvc
.perform(
post("/end_point")
.contentType("application/json")
.content(requestBody))
.andExpect(status().isBadRequest());
I expected a MethodArgumentNotValidException which is handled by a #ControllerAdvice for this exception, but I get HttpMessageNotReadableException when executing the unit test.
My questions:
is it necessary to test if the request body is null?
if 1. is true, how should this be done with Bean Validation?

Seeing your code, you already check if the body is null. In fact #RequestBody has a default parameter required which defaults to true. So no need for Bean validation for that !
Your main issue here seems to be in your test. First of all it is good to write a test to validate your endpoint behavior on null.
However, in your test you does not pass null. You try to create a Json object from a null value with your objectMapper.
The object you are writting seems not to be a valid json. So when your sending this body, Spring says that it cannot read the message, aka the body of your request, as you say it is a application/json content but there is not json in it.
To test null body, just send your request in your test just removing the .content(requestBody) line and it should work !
--- Edit 1
I thought it was rejecting the message because of the body, but in fact it seems to work right away for me. Here is my controler and test so you can compare to your full code :
#RestController()
#RequestMapping("end_point")
public class TestController {
#PostMapping
public ResponseEntity<Map<String, Object>> editFoo(#RequestBody Foo foo) {
// if(foo == null) {
// return (new ResponseEntity<>(new HashMap<>(), HttpStatus.BAD_REQUEST));
// }
return (new ResponseEntity<>(new HashMap<>(), HttpStatus.OK));
}
}
#RunWith(SpringRunner.class)
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
#AutoConfigureMockMvc
public class TestControllerTest {
#Autowired
private MockMvc mvc;
#Autowired
private ObjectMapper objectMapper;
#Test
public void test_body_is_null() throws Exception {
Foo foo = null;
String requestBody = objectMapper.writeValueAsString(foo);
// Act + assert
mvc
.perform(
post("/end_point")
.contentType("application/json")
.content(requestBody))
.andExpect(status().isBadRequest());
}
}
This was made using Spring Boot 2.1.6.RELEASE
--- Edit 2
For the record if you want to use validation for null here, here is a snippet of the controller :
#RestController()
#RequestMapping("end_point")
#Validated
public class TestController {
#PostMapping
public ResponseEntity<Map<String, Object>> editFoo(#NotNull #RequestBody(required = false) Foo foo) {
return (new ResponseEntity<>(new HashMap<>(), HttpStatus.OK));
}
}
First you have to set required to false for the body, as default is true. Then you have to add the #NotNull annotation on the request body and #Validated on the controller.
Here if you launch your test you will see that the request fails with :
org.springframework.web.util.NestedServletException: Request processing failed; nested exception is javax.validation.ConstraintViolationException: editFoo.foo: must not be null
As you said you had a #ControllerAdvice you can then map the exception as you wish !

Related

Calling #RequestBody from jUnit test in spring

I am new to jUnit test.I have a request to test a method that has a #RequestBody object.Searched for almost 4 hours from goodle but cant find it.I also read similar question from stack but that doesnt solved my problem.
My testing controller:
#Test
public void testInsertRequest() throws Exception {
mockMvc.perform(post("http://localhost:8081/requests/add"))
.andExpect(jsonPath("$.date").value("2017-08-09")).andExpect(jsonPath("$.orderPerson").value("Nermin"))
.andExpect(jsonPath("$.orderPhone").value("0777675432")).andExpect(jsonPath("$.client").value("Nezrin"))
.andExpect(jsonPath("$.clientPhone").value("0776763254")).andExpect(jsonPath("$.department").value("IT"))
.andExpect(jsonPath("$.route").value("Neriman Nerimanov"));
}
And method that i want to test:
#RequestMapping(value = "/add", method = RequestMethod.POST)
public String insertRequest(#RequestBody EmployeeRequest employeeRequest) {
System.out.println(employeeRequest.getDate());
requestService.insertRequest(employeeRequest);
}
I get this error:Failed to read HTTP message: org.springframework.http.converter.HttpMessageNotReadableException: Required request body is missing.So how can i call from junit #RequestBody
You should add a request body of the EmployeeRequest type.
EmployeeRequest employeeRequest = new EmployeeRequest(...);
String body = (new ObjectMapper()).valueToTree(employeeRequest).toString();
mockMvc.perform(post("http://localhost:8081/requests/add"))
.content(body)
.andExpect ...

spring boot override default REST exception handler

I am not able to override default spring boot error response in REST api. I have following code
#ControllerAdvice
#Controller
class ExceptionHandlerCtrl {
#ResponseStatus(value=HttpStatus.UNPROCESSABLE_ENTITY, reason="Invalid data")
#ExceptionHandler(BusinessValidationException.class)
#ResponseBody
public ResponseEntity<BusinessValidationErrorVO> handleBusinessValidationException(BusinessValidationException exception){
BusinessValidationErrorVO vo = new BusinessValidationErrorVO()
vo.errors = exception.validationException
vo.msg = exception.message
def result = new ResponseEntity<>(vo, HttpStatus.UNPROCESSABLE_ENTITY);
result
}
Then in my REST api I am throwing this BusinessValidationException. This handler is called (I can see it in debugger) however I still got default spring boot REST error message. Is there a way to override and use default only as fallback? Spring Boot version 1.3.2 with groovy. Best Regards
Remove #ResponseStatus from your method. It creates an undesirable side effect and you don't need it, since you are setting HttpStatus.UNPROCESSABLE_ENTITY in your ResponseEntity.
From the JavaDoc on ResponseStatus:
Warning: when using this annotation on an exception class, or when setting the reason attribute of this annotation, the HttpServletResponse.sendError method will be used.
With HttpServletResponse.sendError, the response is considered complete and should not be written to any further. Furthermore, the Servlet container will typically write an HTML error page therefore making the use of a reason unsuitable for REST APIs. For such cases it is preferable to use a ResponseEntity as a return type and avoid the use of #ResponseStatus altogether.
I suggest you to read this question: Spring Boot REST service exception handling
There you can find some examples that explain how to combine ErrorController/ ControllerAdvice in order to catch any exception.
In particular check this answer:
https://stackoverflow.com/a/28903217/379906
You should probably remove the annotation #ResponseStatus from the method handleBusinessValidationException.
Another way that you have to rewrite the default error message is using a controller with the annotation #RequestMapping("/error"). The controller must implement the ErrorController interface.
This is the error controller that I use in my app.
#RestController
#RequestMapping("/error")
public class RestErrorController implements ErrorController
{
private final ErrorAttributes errorAttributes;
#Autowired
public MatemoErrorController(ErrorAttributes errorAttributes) {
Assert.notNull(errorAttributes, "ErrorAttributes must not be null");
this.errorAttributes = errorAttributes;
}
#Override
public String getErrorPath() {
return "/error";
}
#RequestMapping
public Map<String, Object> error(HttpServletRequest aRequest) {
return getErrorAttributes(aRequest, getTraceParameter(aRequest));
}
private boolean getTraceParameter(HttpServletRequest request) {
String parameter = request.getParameter("trace");
if (parameter == null) {
return false;
}
return !"false".equals(parameter.toLowerCase());
}
private Map<String, Object> getErrorAttributes(HttpServletRequest aRequest, boolean includeStackTrace)
{
RequestAttributes requestAttributes = new ServletRequestAttributes(aRequest);
return errorAttributes.getErrorAttributes(requestAttributes, includeStackTrace);
} }

Content type 'application/x-www-form-urlencoded;charset=UTF-8' not supported for #RequestBody MultiValueMap

Based on the answer for problem with x-www-form-urlencoded with Spring #Controller
I have written the below #Controller method
#RequestMapping(value = "/{email}/authenticate", method = RequestMethod.POST
, produces = {"application/json", "application/xml"}
, consumes = {"application/x-www-form-urlencoded"}
)
public
#ResponseBody
Representation authenticate(#PathVariable("email") String anEmailAddress,
#RequestBody MultiValueMap paramMap)
throws Exception {
if(paramMap == null || paramMap.get("password") == null) {
throw new IllegalArgumentException("Password not provided");
}
}
the request to which fails with the below error
{
"timestamp": 1447911866786,
"status": 415,
"error": "Unsupported Media Type",
"exception": "org.springframework.web.HttpMediaTypeNotSupportedException",
"message": "Content type 'application/x-www-form-urlencoded;charset=UTF-8' not supported",
"path": "/users/usermail%40gmail.com/authenticate"
}
[PS: Jersey was far more friendly, but couldn't use it now given the practical restrictions here]
The problem is that when we use application/x-www-form-urlencoded, Spring doesn't understand it as a RequestBody. So, if we want to use this
we must remove the #RequestBody annotation.
Then try the following:
#RequestMapping(
path = "/{email}/authenticate",
method = RequestMethod.POST,
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
produces = {
MediaType.APPLICATION_ATOM_XML_VALUE,
MediaType.APPLICATION_JSON_VALUE
})
public #ResponseBody Representation authenticate(
#PathVariable("email") String anEmailAddress,
MultiValueMap paramMap) throws Exception {
if (paramMap == null &&
paramMap.get("password") == null) {
throw new IllegalArgumentException("Password not provided");
}
return null;
}
Note that removed the annotation #RequestBody
answer: Http Post request with content type application/x-www-form-urlencoded not working in Spring
It seems that now you can just mark the method parameter with #RequestParam and it will do the job for you.
#PostMapping( "some/request/path" )
public void someControllerMethod( #RequestParam Map<String, String> body ) {
//work with Map
}
Add a header to your request to set content type to application/json
curl -H 'Content-Type: application/json' -s -XPOST http://your.domain.com/ -d YOUR_JSON_BODY
this way spring knows how to parse the content.
In Spring 5
#PostMapping( "some/request/path" )
public void someControllerMethod( #RequestParam MultiValueMap body ) {
// import org.springframework.util.MultiValueMap;
String datax = (String) body .getFirst("datax");
}
#RequestBody MultiValueMap paramMap
in here Remove the #RequestBody Annotaion
#RequestMapping(value = "/signin",method = RequestMethod.POST)
public String createAccount(#RequestBody LogingData user){
logingService.save(user);
return "login";
}
#RequestMapping(value = "/signin",method = RequestMethod.POST)
public String createAccount( LogingData user){
logingService.save(user);
return "login";
}
like that
Simply removing #RequestBody annotation solves the problem (tested on Spring Boot 2):
#RestController
public class MyController {
#PostMapping
public void method(#Valid RequestDto dto) {
// method body ...
}
}
I met the same problem when I want to process my simple HTML form submission (without using thymeleaf or Spring's form tag) in Spring MVC.
The answer of Douglas Ribeiro will work very well. But just in case, for anyone, like me, who really want to use "#RequestBody" in Spring MVC.
Here is the cause of the problem:
Spring need to ① recognize the "Content-Type", and ② convert the
content to the parameter type we declared in the method's signature.
The 'application/x-www-form-urlencoded' is not supported, because, by
default, the Spring cannot find a proper HttpMessageConverter to do
the converting job, which is step ②.
Solution:
We manually add a proper HttpMessageConverter into the Spring's
configuration of our application.
Steps:
Choose the HttpMessageConverter's class we want to use. For
'application/x-www-form-urlencoded', we can choose
"org.springframework.http.converter.FormHttpMessageConverter".
Add the FormHttpMessageConverter object to Spring's configuration,
by calling the "public void
configureMessageConverters(List<HttpMessageConverter<?>>
converters)" method of the "WebMvcConfigurer" implementation class
in our application. Inside the method, we can add any
HttpMessageConverter object as needed, by using "converters.add()".
By the way, the reason why we can access the value by using "#RequestParam" is:
According to Servlet Specification (Section 3.1.1):
The following are the conditions that must be met before post form
data will be populated to the parameter set: The request is an HTTP
or HTTPS request. 2. The HTTP method is POST. 3. The content type is
application/x-www-form-urlencoded. 4. The servlet has made an initial
call of any of the getParameter family of methods on the request
object.
So, the value in request body will be populated to parameters. But in Spring, you can still access RequestBody, even you can use #RequstBody and #RequestParam at the same method's signature.
Like:
#RequestMapping(method = RequestMethod.POST, consumes = {MediaType.APPLICATION_FORM_URLENCODED_VALUE})
public String processForm(#RequestParam Map<String, String> inputValue, #RequestBody MultiValueMap<String, List<String>> formInfo) {
......
......
}
The inputValue and formInfo contains the same data, excpet for the type for "#RequestParam" is Map, while for "#RequestBody" is MultiValueMap.
I wrote about an alternative in this StackOverflow answer.
There I wrote step by step, explaining with code. The short way:
First: write an object
Second: create a converter to mapping the model extending the AbstractHttpMessageConverter
Third: tell to spring use this converter implementing a WebMvcConfigurer.class overriding the configureMessageConverters method
Fourth and final: using this implementation setting in the mapping inside your controller the consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE and #RequestBody in front of your object.
I'm using spring boot 2.
#PostMapping(path = "/my/endpoint", consumes = { MediaType.APPLICATION_FORM_URLENCODED_VALUE })
public ResponseEntity<Void> handleBrowserSubmissions(MyDTO dto) throws Exception {
...
}
That way works for me
You can try to turn support on in spring's converter
#EnableWebMvc
#Configuration
public class WebConfig implements WebMvcConfigurer {
#Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
// add converter suport Content-Type: 'application/x-www-form-urlencoded'
converters.stream()
.filter(AllEncompassingFormHttpMessageConverter.class::isInstance)
.map(AllEncompassingFormHttpMessageConverter.class::cast)
.findFirst()
.ifPresent(converter -> converter.addSupportedMediaTypes(MediaType.APPLICATION_FORM_URLENCODED_VALUE));
}
}
Just add an HTTP Header Manager if you are testing using JMeter :

Empty Exception Body in Spring MVC Test

I am having trouble while trying to make MockMvc to include the exception message in the response body. I have a controller as follows:
#RequestMapping("/user/new")
public AbstractResponse create(#Valid NewUserParameters params, BindingResult bindingResult) {
if (bindingResult.hasErrors()) throw BadRequestException.of(bindingResult);
// ...
}
where BadRequestException looks sth like this:
#ResponseStatus(value = HttpStatus.BAD_REQUEST, reason = "bad request")
public class BadRequestException extends IllegalArgumentException {
public BadRequestException(String cause) { super(cause); }
public static BadRequestException of(BindingResult bindingResult) { /* ... */ }
}
And I run the following test against /user/new controller:
#Test
public void testUserNew() throws Exception {
getMockMvc().perform(post("/user/new")
.param("username", username)
.param("password", password))
.andDo(print())
.andExpect(status().isOk());
}
which prints the following output:
Resolved Exception:
Type = controller.exception.BadRequestException
ModelAndView:
View name = null
View = null
Model = null
FlashMap:
MockHttpServletResponse:
Status = 400
Error message = bad request
Headers = {X-Content-Type-Options=[nosniff], X-XSS-Protection=[1; mode=block], Cache-Control=[no-cache, no-store, max-age=0, must-revalidate], Pragma=[no-cache], Expires=[0], X-Frame-Options=[DENY]}
Content type = null
Body =
Forwarded URL = null
Redirected URL = null
Cookies = []
Does anybody have an idea on why is Body missing in the print() output?
Edit: I am not using any custom exception handlers and the code works as expected when I run the server. That is, running the application and making the same request to the server returns back
{"timestamp":1423076185822,
"status":400,
"error":"Bad Request",
"exception":"controller.exception.BadRequestException",
"message":"binding failed for field(s): password, username, username",
"path":"/user/new"}
as expected. Hence, there is a problem with the MockMvc I suppose. It somehow misses to capture the message field of the exception, whereas the default exception handler of the regular application server works as expected.
After opening a ticket for the issue, I was told that the error message in the body is taken care of by Spring Boot which configures error mappings at the Servlet container level and since Spring MVC Test runs with a mock Servlet request/response, there is no such error mapping. Further, they recommended me to create at least one #WebIntegrationTest and stick to Spring MVC Test for my controller logic.
Eventually, I decided to go with my own custom exception handler and stick to MockMvc for the rest as before.
#ControllerAdvice
public class CustomExceptionHandler {
#ExceptionHandler(Throwable.class)
public #ResponseBody
ExceptionResponse handle(HttpServletResponse response, Throwable throwable) {
HttpStatus status = Optional
.ofNullable(AnnotationUtils.getAnnotation(throwable.getClass(), ResponseStatus.class))
.map(ResponseStatus::value)
.orElse(HttpStatus.INTERNAL_SERVER_ERROR);
response.setStatus(status.value());
return new ExceptionResponse(throwable.getMessage());
}
}
#Data
public class ExceptionResponse extends AbstractResponse {
private final long timestamp = System.currentTimeMillis();
private final String message;
#JsonCreator
public ExceptionResponse(String message) {
checkNotNull(message, "message == NULL");
this.message = message;
}
}
This likely means that you either didn't handle the exception or you've really left the body empty. To handle the exception either add an error handler in the controller
#ExceptionHandler
public #ResponseBody String handle(BadRequestException e) {
return "I'm the body";
}
or user the global error handler if you're on 3.2 or above
#ControllerAdvice
public class GlobalExceptionHandler {
#ExceptionHandler
public #ResponseBody String handleBadRequestException(BadRequestException ex) {
return "I'm the body";
}
}
with this the body will be populate, you should populate it with your error message
Updated solution:
If you don't want to do a full integration test but still want to make sure the message is as expected, you can still do the following:
String errorMessage = getMockMvc()
.perform(post("/user/new"))
...
.andReturn().getResolvedException().getMessage();
assertThat(errorMessage, is("This is the error message!");

MocMVC giving HttpMessageNotReadableException

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?
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.
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.
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
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>

Resources