-
Notifications
You must be signed in to change notification settings - Fork 40.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
MockMvc support for testing errors #5574
Comments
I'm not sure that we should be going out of our way to encourage people to test for errors in this way. It's testing that Boot's error page support is working, rather than testing a user's own code. IMO, in most cases it'll be sufficient to verify that the request has been forwarded to That said, I have encountered this problem before when trying to provide documentation for the JSON error response. The difficulty is that MockMvc doesn't fully support forwarding requests which is what the error page support uses. I worked around the problem by making a MockMvc call to
|
@wilkinsona Thanks for the response.
The goal is to ensure that user's have everything configured correctly. This becomes more important when the user configures custom error handling.
This is a good point. However, the MockMvc and HtmlUnit support does handle forwards. Granted, it does not handle error codes but perhaps this is something that should change. Ultimately, my goal is to easily be able to test custom error handling in Boot. I want to be able to ensure that if my application has an error it is properly handled (not just if I directly request a URL for error handling). |
I have a similar issue with the verification of the |
I got caught by that as well when writing a test for #7582 |
As the |
We discussed it briefly in the context of the 1.5 release but we don't think we can find a suitable solution in the time-frame. We'll need to look again once 1.5 is released. |
Why can I not disable the |
Hi, I'm trying to document a service using REST Docs and I too, have this problem. 😄 In some conditional case, the value returned from the API expires and I'd like to show the error case in the documentation. Currently I'm showing what the error response looks like using the above code sample @wilkinsona shared. |
To be sure that any error handling is working fully, it's necessary to involve the servlet container in that testing as it's responsible for error page registration etc. Even if MockMvc itself or a Boot enhancement to MockMvc allowed forwarding to an error page, you'd be testing the testing infrastructure not the real-world scenario that you're actually interested in. Our recommendation for tests that want to be sure that error handling is working correctly, is to use an embedded container and test with WebTestClient, RestAssured, or TestRestTemplate. |
As it properly said above, when RestDocs came, error handling became important and mockMvc won't used only for the testing, but for the test-driven documentation. And it's very sad if one cannot document some important error case. I think the approach proposed by @wilkinsona is good, we can just make manual redirect to error page.
|
For anyone who's still struggling with this, I found super easy solution. import org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController
import org.springframework.boot.web.servlet.error.ErrorController
import org.springframework.http.ResponseEntity
import org.springframework.validation.BindException
import org.springframework.web.bind.MethodArgumentNotValidException
import org.springframework.web.bind.annotation.ControllerAdvice
import org.springframework.web.bind.annotation.ExceptionHandler
import javax.servlet.http.HttpServletRequest
/**
* This advice is necessary because MockMvc is not a real servlet environment, therefore it does not redirect error
* responses to [ErrorController], which produces validation response. So we need to fake it in tests.
* It's not ideal, but at least we can use classic MockMvc tests for testing error response + document it.
*/
@ControllerAdvice
internal class MockMvcValidationConfiguration(private val errorController: BasicErrorController) {
// add any exceptions/validations/binding problems
@ExceptionHandler(MethodArgumentNotValidException::class, BindException::class)
fun defaultErrorHandler(request: HttpServletRequest, ex: Exception): ResponseEntity<*> {
request.setAttribute("javax.servlet.error.request_uri", request.pathInfo)
request.setAttribute("javax.servlet.error.status_code", 400)
return errorController.error(request)
}
} |
Thanks for the tips @jmisur. I've used your solution in my CrashControllerTest |
|
I have modified @jmisur solution, it works for all kind of exception, I am not satisfied with json conversion but if I find any better way I can update it. @TestConfiguration
public class MockMvcRestExceptionConfiguration implements WebMvcConfigurer {
private final BasicErrorController errorController;
public MockMvcRestExceptionConfiguration(final BasicErrorController basicErrorController) {
this.errorController = basicErrorController;
}
@Override
public void addInterceptors(final InterceptorRegistry registry) {
registry.addInterceptor(
new HandlerInterceptor() {
@Override
public void afterCompletion(
final HttpServletRequest request,
final HttpServletResponse response,
final Object handler,
final Exception ex)
throws Exception {
final int status = response.getStatus();
if (status >= 400) {
request.setAttribute(RequestDispatcher.ERROR_STATUS_CODE, status);
new ObjectMapper()
.writeValue(
response.getOutputStream(),
MockMvcRestExceptionConfiguration.this
.errorController
.error(request)
.getBody());
}
}
});
}
} |
This worked for me but changing from |
This is my solution based on @jmisur using a ControllerAdvise package org.eu.rubensa.springboot.error.common;
import java.util.Map;
import org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController;
import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import org.springframework.web.util.WebUtils;
/**
* This advice is necessary because MockMvc is not a real servlet environment,
* therefore it does not redirect error responses to {@link ErrorController},
* which produces validation response. So we need to fake it in tests. It's not
* ideal, but at least we can use classic MockMvc tests for testing error
* response + document it.
*/
@ControllerAdvice
public class MockMvcRestExceptionControllerAdvise extends ResponseEntityExceptionHandler {
BasicErrorController errorController;
public MockMvcRestExceptionControllerAdvise(BasicErrorController errorController) {
this.errorController = errorController;
}
/**
* Handle any generic {@link Exception} not handled by
* {@link ResponseEntityExceptionHandler}
*
* @param ex
* @param request
* @return
* @throws Exception
*/
@ExceptionHandler({ Exception.class })
public final ResponseEntity<Object> handleGenericException(Exception ex, WebRequest request) throws Exception {
HttpHeaders headers = new HttpHeaders();
HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;
ResponseStatus responseStatus = AnnotationUtils.findAnnotation(ex.getClass(), ResponseStatus.class);
if (responseStatus != null) {
status = responseStatus.value();
}
return handleExceptionInternal(ex, null, headers, status, request);
}
/**
* Overrides
* {@link ResponseEntityExceptionHandler#handleExceptionInternal(Exception,
* Object, HttpHeaders, HttpStatus, WebRequest))} to expose error attributes to
* {@link BasicErrorController} and uses it to handle the response.
*
* @param ex
* @param body
* @param headers
* @param status
* @param request
* @return ResponseEntity<Object>
*/
@Override
protected ResponseEntity<Object> handleExceptionInternal(Exception ex, Object body, HttpHeaders headers,
HttpStatus status, WebRequest request) {
request.setAttribute(WebUtils.ERROR_STATUS_CODE_ATTRIBUTE, status.value(), WebRequest.SCOPE_REQUEST);
request.setAttribute(WebUtils.ERROR_REQUEST_URI_ATTRIBUTE,
((ServletWebRequest) request).getRequest().getRequestURI().toString(), WebRequest.SCOPE_REQUEST);
request.setAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE, ex, WebRequest.SCOPE_REQUEST);
request.setAttribute(WebUtils.ERROR_MESSAGE_ATTRIBUTE, ex.getMessage(), WebRequest.SCOPE_REQUEST);
ResponseEntity<Map<String, Object>> errorControllerResponeEntity = errorController
.error(((ServletWebRequest) request).getRequest());
return new ResponseEntity<>(errorControllerResponeEntity.getBody(), errorControllerResponeEntity.getHeaders(),
errorControllerResponeEntity.getStatusCode());
}
} |
And this is my solution based on @firatkucuk using a custom @TestConfiguration with a HandlerInterceptor @TestConfiguration
public class MockMvcRestExceptionConfiguration implements WebMvcConfigurer {
private final BasicErrorController errorController;
public MockMvcRestExceptionConfiguration(final BasicErrorController basicErrorController) {
this.errorController = basicErrorController;
}
@Override
public void addInterceptors(final InterceptorRegistry registry) {
registry.addInterceptor(new HandlerInterceptor() {
@Override
public void afterCompletion(final HttpServletRequest request, final HttpServletResponse response,
final Object handler, final Exception ex) throws Exception {
final int status = response.getStatus();
if (status >= 400) {
request.setAttribute(RequestDispatcher.ERROR_STATUS_CODE, status);
request.setAttribute(WebUtils.ERROR_STATUS_CODE_ATTRIBUTE, status);
request.setAttribute(WebUtils.ERROR_REQUEST_URI_ATTRIBUTE, request.getRequestURI().toString());
// The original exception is already saved as an attribute request
Exception exception = (Exception) request.getAttribute(DispatcherServlet.EXCEPTION_ATTRIBUTE);
if (exception != null) {
request.setAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE, exception);
request.setAttribute(WebUtils.ERROR_MESSAGE_ATTRIBUTE, exception.getMessage());
}
new ObjectMapper().writeValue(response.getOutputStream(),
MockMvcRestExceptionConfiguration.this.errorController.error(request).getBody());
}
}
});
}
} |
The The |
Unfortunately, the workarounds suggested for this bug appear to be limited to |
|
Spring Boot currently registers an endpoint with the servlet container to process errors. This means that
MockMvc
cannot be used to assert the errors. For example, the following will fail:with:
despite the fact that the message is displayed when the application is actually running. You can view this problem in https://github.com/rwinch/boot-mockmvc-error
It would be nice if Spring Boot could provide hooks to enable testing of the error pages too.
The text was updated successfully, but these errors were encountered: