I’m trying write a unit test for an ExceptionHandler class but I’m not 100% sure how to go about it. I’m struggling to work out how do I go about setting up the MethodArgumentNotValidException class with an error list?
#ControllerAdvice
public class InputExceptionHandler extends ResponseEntityExceptionHandler {
private static final String INVALID_CHARACTER_MESSAGE = "Fields contain invalid characters:";
#Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex,
HttpHeaders headers,
HttpStatus status,
WebRequest request) {
final List<String> errorList = getErrorList(ex);
final StringBuilder errorMessage = new StringBuilder(popFirstError(errorList));
if(errorMessage.toString().contains(INVALID_CHARACTER_MESSAGE)) {
combineDuplicateErrorMessage(errorList, errorMessage);
}
return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY).body(makeBankDetailsValidationModel(errorMessage.toString()));
}
private BankDetailsValidationModel makeBankDetailsValidationModel(String validationErrorDescription){
return new BankDetailsValidationModel(false, false, validationErrorDescription, "");
}
private StringBuilder combineDuplicateErrorMessage(List<String> errorList, StringBuilder errorMessage) {
return errorList.stream()
.filter(value -> value.contains(INVALID_CHARACTER_MESSAGE))
.findFirst()
.map(value -> errorMessage.append(", ").append(value.substring(35)))
.orElse(null);
}
private String popFirstError(List<String> errorMessages) {
final String message = errorMessages.get(0);
errorMessages.remove(0);
return message;
}
private List<String> getErrorList(MethodArgumentNotValidException ex) {
return ex.getBindingResult()
.getAllErrors()
.stream()
.map(ObjectError::getDefaultMessage)
.sorted()
.collect(Collectors.toCollection(ArrayList::new));
}
}
Since you're only testing your handler and not the invocation (that's Sping's job) why not just using a mock-Exception.
This way you could mock whatever properties your Exception needs to cover all the paths without having to com up with ways to create the Exception in first place...
Related
I have around 20 to 30 different types of different Exceptions all extending Exception class in Java.
one example is:
public class NoHandlerFoundException extends Exception {
private static final long serialVersionUID = -9079454849611061074L;
public NoHandlerFoundException() {
super();
}
public NoHandlerFoundException(final String message) {
super(message);
}
}
Other example is:
public class ResourceNotFoundException extends Exception{
private static final long serialVersionUID = -9079454849611061074L;
public ResourceNotFoundException() {
super();
}
public ResourceNotFoundException(final String message) {
super(message);
}
}
and many more.
As you can see most of the code is repeated and then I use ControllerAdvice like (I know code in ControllerAdvice argument exception class should be proper):
#ExceptionHandler({NoHandlerFoundException.class, ResourceNotFoundException.class})
#ResponseStatus(value = HttpStatus.NOT_FOUND)
public #ResponseBody ExceptionResponse handleResourceNotFound(final NoHandlerFoundException exception,
final HttpServletRequest request) {
ExceptionResponse error = new ExceptionResponse();
error.setErrorMessage(exception.getMessage());
error.callerURL(request.getRequestURI());
return error;
}
So I want to know if we have any way in which I can optimize above exceptions and not write individual classes doing almost same job n times of times but still want to differentiate between them.
Thank you.
You can use below approach to reduce code redundancy.
#ExceptionHandler(value = {NoHandlerFoundException.class, ResourceNotFoundException.class})
protected ResponseEntity handleInvalidDataException(RuntimeException exception, WebRequest request) {
ExceptionResponse error = new ExceptionResponse();
error.setErrorMessage(exception.getMessage());
error.callerURL(request.getRequestURI());
return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
}
In my route I have one Post endpoint for which I expecting to accept the list of strings which I will then proccessing in handler.
My question is, how can I get these list of strings from ServerRequest body and iterate over them using Flux?
My Router
#Configuration
public class TestUrlRouter {
#Bean
public RouterFunction<ServerResponse> routes(TestUrlHandler handler) {
return RouterFunctions.route(
RequestPredicates.POST("/urls").and(RequestPredicates.accept(MediaType.APPLICATION_JSON)),
handler::testUrls
);
}
}
My handler
#Component
public class TestUrlHandler {
#Autowired
private TestUrlService testUrlService;
public Mono<ServerResponse> testUrls(ServerRequest request) {
request.bodyToFlux(List.class) // how to iterate over strings?
}
}
I finally solved it by this code:
#Component
public class TestUrlHandler {
#Autowired
private TestUrlService testUrlService;
public Mono<ServerResponse> testUrls(ServerRequest request) {
ParallelFlux<TestUrlResult> results = request.bodyToMono(String[].class)
.flatMapMany(Flux::fromArray)
.flatMap(url -> testUrlService.testUrls(url))
.doOnComplete(() -> System.out.println("Testing of URLS is done."));
return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(results, TestUrlResult.class);
}
}
There are lot of ways of achieving it.
Keeping it simple(modify as per your need).
protected static class WrapperList{
private List<String> urls;
// getter & setter
}
public Mono<ServerResponse> testUrls(ServerRequest request) {
return request.bodyToFlux(WrapperList.class).flatMap(wrapperList -> {
wrapperList.getUrls().stream().forEach(System.out::println);
return ServerResponse.ok().build();
}).take(1).next();
}
request payload:
{
"urls": ["url1","url2"]
}
I have a service method that returns a UserDto object. However there are several situations where the request to the controller would return a HTTP status other than 200 but the service can only return null or the UserDto.
Is it bad practice to move some of the business logic to the controller and call the repository directly in order to return more detailed error messages, since the service cannot pass back an error message to the contoller?
You can do a bit better by having the service throw business exception, and the controller react on that. For example, a CustomerService could throw a `CustomerNotFoundException', and the controller could turn that into an appropriate HTTP status code, like this:
#ExceptionHandler({ CustomerNotFoundException.class })
public ResponseEntity handleException(CustomerNotFoundException ex, WebRequest request) {
ErrorDetails errorDetails = new ErrorDetails(new Date(), ex.getMessage(), request.getDescription(false));
return new ResponseEntity<>(errorDetails, HttpStatus.NOT_FOUND);
}
I recommend against moving the business logic to the controller, since the controller is more of an infrastructure component than domain logic. Also, consider adding another protocol, for example, a binary protocol, which wouldn't use the controller layer. You might miss your validations or business rules.
You can also use #ControllerAdvice from Spring to handle such cases, take a look at the below code.I hope this should help you return detailed error messages to the the controller.
#Order(Ordered.HIGHEST_PRECEDENCE)
#ControllerAdvice
public class ApiExceptionHandler extends ResponseEntityExceptionHandler {
#ExceptionHandler(NoSuchUserException.class)
public ResponseEntity<Object> handleNoSuchPinCodeException(
NoSuchUserException ex) {
ApiError apiError = new ApiError(HttpStatus.NOT_FOUND);
apiError.setErrorMessage(ex.getMessage());
return buildResponseEntity(apiError);
}
private ResponseEntity<Object> buildResponseEntity(ApiError apiError) {
return new ResponseEntity<>(apiError, apiError.getStatus());
}
}
public class NoSuchUserException extends Exception{
public NoSuchUserException (String message) {
super(message);
}
}
public class ApiError {
private HttpStatus status;
private String errorMessage;
private ApiError() {
}
public ApiError(HttpStatus status) {
this();
this.status = status;
}
public ApiError(HttpStatus status, String errorMessage, Throwable ex) {
this();
this.status = status;
this.errorMessage = errorMessage;
}
public HttpStatus getStatus() {
return status;
}
public void setStatus(HttpStatus status) {
this.status = status;
}
public String getErrorMessage() {
return errorMessage;
}
public void setErrorMessage(String errorMessage) {
this.errorMessage = errorMessage;
}
}
I have the following #RestController
#RequestMapping(...)
public ResponseEntity(#RequestBody #Valid SomeDTO, BindingResult errors) {
//do something with errors if validation error occur
}
public class SomeDTO {
public SomeEnum someEnum;
}
If the JSON request is { "someEnum": "valid value" }, everything works fine. However, if the request is { "someEnum": "invalid value" }, it only return error code 400.
How can I trap this error so I can provide a custom error message, such as "someEnum must be of value A/B/C".
The answer provided by #Amit is good and works. You can go ahead with that if you want to deserialize an enum in a specific way. But that solution is not scalable. Because every enum which needs validation must be annotated with #JsonCreator.
Other answers won't help you beautify the error message.
So here's my solution generic to all the enums in spring web environment.
#RestControllerAdvice
public class ControllerErrorHandler extends ResponseEntityExceptionHandler {
public static final String BAD_REQUEST = "BAD_REQUEST";
#Override
public ResponseEntity<Object> handleHttpMessageNotReadable(HttpMessageNotReadableException exception,
HttpHeaders headers, HttpStatus status, WebRequest request) {
String genericMessage = "Unacceptable JSON " + exception.getMessage();
String errorDetails = genericMessage;
if (exception.getCause() instanceof InvalidFormatException) {
InvalidFormatException ifx = (InvalidFormatException) exception.getCause();
if (ifx.getTargetType()!=null && ifx.getTargetType().isEnum()) {
errorDetails = String.format("Invalid enum value: '%s' for the field: '%s'. The value must be one of: %s.",
ifx.getValue(), ifx.getPath().get(ifx.getPath().size()-1).getFieldName(), Arrays.toString(ifx.getTargetType().getEnumConstants()));
}
}
ErrorResponse errorResponse = new ErrorResponse();
errorResponse.setTitle(BAD_REQUEST);
errorResponse.setDetail(errorDetails);
return handleExceptionInternal(exception, errorResponse, headers, HttpStatus.BAD_REQUEST, request);
}
}
This will handle all the invalid enum values of all types and provides a better error message for the end user.
Sample output:
{
"title": "BAD_REQUEST",
"detail": "Invalid enum value: 'INTERNET_BANKING' for the field: 'paymentType'. The value must be one of: [DEBIT, CREDIT]."
}
#ControllerAdvice
public static class GenericExceptionHandlers extends ResponseEntityExceptionHandler {
#Override
protected ResponseEntity<Object> handleHttpMessageNotReadable(HttpMessageNotReadableException e, HttpHeaders headers, HttpStatus status, WebRequest request) {
return new ResponseEntity<>(new ErrorDTO().setError(e.getMessage()), HttpStatus.BAD_REQUEST);
}
}
I created a fully functional Spring boot Application with a Test on Bitbucket
You do not need #Valid for enum validation, you can achieve the required response using below code:
Controller Code, StackDTO has an enum PaymentType in it:
#RequestMapping(value = "/reviews", method = RequestMethod.POST)
#ResponseBody
public ResponseEntity<String> add(#RequestBody StackDTO review) {
return new ResponseEntity<String>(HttpStatus.ACCEPTED);
}
Create an exception class, as EnumValidationException
public class EnumValidationException extends Exception {
private String enumValue = null;
private String enumName = null;
public String getEnumValue() {
return enumValue;
}
public void setEnumValue(String enumValue) {
this.enumValue = enumValue;
}
public String getEnumName() {
return enumName;
}
public void setEnumName(String enumName) {
this.enumName = enumName;
}
public EnumValidationException(String enumValue, String enumName) {
super(enumValue);
this.enumValue = enumValue;
this.enumName = enumName;
}
public EnumValidationException(String enumValue, String enumName, Throwable cause) {
super(enumValue, cause);
this.enumValue = enumValue;
this.enumName = enumName;
}
}
I have enum as below, with a special annotation #JsonCreator on a method create
public enum PaymentType {
CREDIT("Credit"), DEBIT("Debit");
private final String type;
PaymentType(String type) {
this.type = type;
}
String getType() {
return type;
}
#Override
public String toString() {
return type;
}
#JsonCreator
public static PaymentType create (String value) throws EnumValidationException {
if(value == null) {
throw new EnumValidationException(value, "PaymentType");
}
for(PaymentType v : values()) {
if(value.equals(v.getType())) {
return v;
}
}
throw new EnumValidationException(value, "PaymentType");
}
}
Finally RestErrorHandler class,
#ControllerAdvice
public class RestErrorHandler {
#ExceptionHandler(HttpMessageNotReadableException.class)
#ResponseStatus(HttpStatus.BAD_REQUEST)
#ResponseBody
public ResponseEntity<ValidationErrorDTO> processValidationIllegalError(HttpMessageNotReadableException ex,
HandlerMethod handlerMethod, WebRequest webRequest) {
EnumValidationException exception = (EnumValidationException) ex.getMostSpecificCause();
ValidationErrorDTO errorDTO = new ValidationErrorDTO();
errorDTO.setEnumName(exception.getEnumName());
errorDTO.setEnumValue(exception.getEnumValue());
errorDTO.setErrorMessage(exception.getEnumValue() + " is an invalid " + exception.getEnumName());
return new ResponseEntity<ValidationErrorDTO>(errorDTO, HttpStatus.BAD_REQUEST);
}
}
ValidationErrorDTO is the dto with setters/getters of enumValue, enumName and errorMessage. Now when you send POST call to controller endpoint /reviews with below request
{"paymentType":"Credit2"}
Then code returns response as 400 with below response body -
{
"enumValue": "Credit2",
"enumName": "PaymentType",
"errorMessage": "Credit2 is an invalid PaymentType"
}
Let me know if it resolves your issue.
Yon can achieve this using #ControllerAdvice as follows
#org.springframework.web.bind.annotation.ExceptionHandler(value = {InvalidFormatException.class})
public ResponseEntity handleIllegalArgumentException(InvalidFormatException exception) {
return ResponseEntity.badRequest().body(exception.getMessage());
}
Basically , the idea is to catch com.fasterxml.jackson.databind.exc.InvalidFormatException and handle it as per your requirement.
#Valid has to do with Hibernate bean validation. Currently enum type is not supported out of the box. I found this answer to be the closet, https://funofprograming.wordpress.com/2016/09/29/java-enum-validator/, the drawback however is that you have to make the enum field of type String instead.
I have a controller like below
#Controller("myController")
#RequestMapping("api")
public class MyController {
#RequestMapping(method = RequestMethod.GET, value = "/get/info/{id}", headers = "Accept=application/json")
public #ResponseBody
Student getInfo(#PathVariable String info) {
.................
}
#ExceptionHandler(Throwable.class)
#ResponseStatus( HttpStatus.EXPECTATION_FAILED)
#ResponseBody
public String handleIOException(Throwable ex) {
ErrorResponse errorResponse = errorHandler.handelErrorResponse(ex);
return errorResponse.toString();
}
}
The controller has an error handling mechanism, in the error handling option it always return expectation fail status code 417. But I need to set a dynamic error Http status code like 500, 403 etc depending on type of error. How do I do this?
You need to change the type of the output value ResponseEntity. Answer here:
How to respond with HTTP 400 error in a Spring MVC #ResponseBody method returning String?
I get a solution and going to share this and also like to know any good suggestions.
#Controller("myController")
#RequestMapping("api")
public class MyController {
#RequestMapping(method = RequestMethod.GET, value = "/get/info/{id}", headers = "Accept=application/json")
public #ResponseBody
Student getInfo(#PathVariable String info) {
// ...
}
}
// ...
#ExceptionHandler(Throwable.class)
//#ResponseStatus( HttpStatus.EXPECTATION_FAILED)<<remove this line
#ResponseBody
public String handleIOException(HttpServletResponse httpRes,Throwable ex){ // <<Change this
if (some condition) {
httpRes.setStatus(HttpStatus.BAD_GATEWAY.value());
} else {
httpRes.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
}
ErrorResponse errorResponse = errorHandler.handleErrorResponse(ex);
return errorResponse.toString();
}
Expected out in rest client :
502 Bad Gateway
{
"status":"BAD_GATEWAY",
"error":"java.lang.UnsupportedOperationException",
"message":"Some error message"
}
Thanks for your replies. I still need pointers for good practices.
Going by the code above, you need to be more careful about which exceptions you are throwing and handling. Setting up an exception handler for Throwable seems overly broad.
The way I do this is to create an ErrorMessage class with my XML/JSON marshalling annotations.
#XmlRootElement(name = "error")
public class ErrorMessage {
private Throwable exception;
private String message;
public ErrorMessage() {
this.message = "";
}
public ErrorMessage(String message) {
this.message = message;
}
public ErrorMessage(Throwable exception) {
this.exception = exception;
this.message = exception.getLocalizedMessage();
}
#XmlTransient
#JsonIgnore
public Throwable getException() {
return exception;
}
public void setException(Throwable exception) {
this.exception = exception;
}
#XmlElement(name = "message")
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
With that in place, I tend to create my own application exceptions and then create my exception handler methods such as:
#ExceptionHandler(ResourceNotFoundException.class)
#ResponseBody
#ResponseStatus(HttpStatus.NOT_FOUND)
public ErrorMessage handleResourceNotFoundException(ResourceNotFoundException e, HttpServletRequest req) {
return new ErrorMessage(e);
}
#ExceptionHandler(InternalServerErrorException.class)
#ResponseBody
#ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ErrorMessage handleInternalServerErrorException(InternalServerErrorException e, HttpServletRequest req) {
return new ErrorMessage(e);
}
With those in place, I just need to throw appropriate exceptions from my controller methods. For instance, if I throw a ResourceNotFoundException, then Spring will redirect that to my handleResourceNotFoundException method, which returns a 404, and that will also return JSON or XML representing the error.
You can use an Aspect for your API. If you define an #Around interceptor for your service, you can change the response content.