Cannot properly test ErrorController Spring Boot - spring

due to this tutorial - https://www.baeldung.com/spring-boot-custom-error-page I wanted to customize my error page ie. when someone go to www.myweb.com/blablablalb3 I want to return page with text "wrong url request".
All works fine:
#Controller
public class ApiServerErrorController implements ErrorController {
#Override
public String getErrorPath() {
return "error";
}
#RequestMapping("/error")
public String handleError() {
return "forward:/error-page.html";
}
}
But I dont know how to test it:
#Test
public void makeRandomRequest__shouldReturnErrorPage() throws Exception {
this.mockMvc.perform(get(RANDOM_URL))
.andDo(print());
}
print() returns:
MockHttpServletResponse:
Status = 404
Error message = null
Headers = {X-Application-Context=[application:integration:-1]}
Content type = null
Body =
Forwarded URL = null
Redirected URL = null
Cookies = []
So I cant created something like this:
.andExpect(forwardedUrl("error-page"));
because it fails, but on manual tests error-page is returned.

Testing of a custom ErrorController with MockMvc is unfortunately not supported.
For a detailed explanation, see the official recommendation from the Spring Boot team (source).
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.

My suggestion is to use #ControllerAdvice
In this way you can work around the problem and you can continue to use MockMvc with the big advantage that you are not required to have a running server.
Of course to test explicitly the error page management you need a running server. My suggestion is mainly for those who implemented ErrorController but still want to use MockMvc for unit testing.
#ControllerAdvice
public class MyControllerAdvice {
#ExceptionHandler(FileSizeLimitExceededException.class)
public ResponseEntity<Throwable> handleFileException(HttpServletRequest request, FileSizeLimitExceededException ex) {
return new ResponseEntity<>(ex, HttpStatus.PAYLOAD_TOO_LARGE);
}
#ExceptionHandler(Throwable.class)
public ResponseEntity<Throwable> handleUnexpected(HttpServletRequest request, Throwable throwable) {
return new ResponseEntity<>(throwable, HttpStatus.INTERNAL_SERVER_ERROR);
}
}

Related

How to write junit test cases for below exception handling code?

#PostMapping("/addCompany")
public Company createCompany(#Valid #RequestBody Company company, Errors errors) {
if (errors.hasErrors()) {
throw new ValidationException(errors.getFieldError().getDefaultMessage());
}
return companyService.addCompany(company);
}
You can test it in two ways:
unit test of controller
web layer test
Unit test:
Mock Errors and call createCompany with that mock as an argument.
Verify that exception is thrown
Web layer test:
post an invalid request with mockMvc
check that error code is returned
#WebMvcTest(CompanyController.class)
class CompanyControllerTest {
private static final String CREATE_COMPANY_ENDPOINT = "/addCompany/";
#Autowired
private MockMvc mockMvc;
#Test
void respondsWith4xxOnInvalidCreateCompanyRequest() throws Exception {
String invalidRequest = "{\"id\":\"aaa\", \"name\":\"bbb\"}"
MockHttpServletRequestBuilder builder = MockMvcRequestBuilders
.post(CREATE_COMPANY_ENDPOINT)
.content(invalidRequest)
.contentType(MediaType.APPLICATION_JSON_VALUE);
this.mockMvc.perform(builder)
.andExpect(status().is4xxClientError());
}
}
In this case, I have a strong preference to web layer test - you check actual validation logic. Mocking Errors buys you nothing except code coverage.
On top of that:
Your handling of validation errors is trivial. If you drop Errors from the parameter list, on invalid input Spring will throw MethodArgumentNotValidException with BindingResult, which has all the data you need.

Spring Boot doesn't show error pages implementing a custom ErrorController

I'm trying to show custom error pages depending on the HTTP status code. What I have done is implementing Spring's ErrorController interface in a CustomErrorController but it seems that Spring Boot is not recognizing it.
I have followed this tutorial to do that: https://www.baeldung.com/spring-boot-custom-error-page (section 3.1).
There I have read that first you need to get rid of the famous Spring's default Whitelabel Error Page. So I did this:
#SpringBootApplication(exclude = { ErrorMvcAutoConfiguration.class })
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
This seems to work since the Whitelabel error page hasn't appeared anymore but now when an error happens the Apache Tomcat error page (that ugly one with the stack trace included) appears instead of mine.
Then I've just implemented my CustomErrorController like this:
#Controller
#RequestMapping("/error")
public class CustomErrorController implements ErrorController {
#RequestMapping
public String handleError(HttpServletRequest request) {
Integer statusCode = (Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
if (statusCode != null) {
// Specific error page
return "redirect:/error/" + statusCode;
}
// Global error page
return "error/error";
}
#Override
public String getErrorPath() {
return "/error";
}
#GetMapping("/404")
public String notFoundErrorPage() {
return "error/404";
}
// Other error codes mapping methods
}
I'm using Thymeleaf and my error views are under src/main/resources/views/error, where every specific error page name follows the recommended format of <error_code>.html so, for instance, a 404 error would have a 404.html page associated.
I haven't had any problem with other application views resolving so far. Actually, I have configured my Spring Security to call the /error/403 endpoint if access denied occurs and the error page is shown properly.
Same happens with /error/500, that is called when an internal server exception occurs since I have also implemented the following #ControllerAdvice #ExceptionHandler method:
#ControllerAdvice
#Log4j2
public class GlobalDefaultExceptionHandler {
#ExceptionHandler(Exception.class)
public String defaultErrorHandler(Exception exception) throws Exception {
if (AnnotationUtils.findAnnotation(exception.getClass(), ResponseStatus.class) != null) {
throw exception;
}
log.catching(exception);
return "redirect:/error/500";
}
}
So, if each of these endpoints works individually, why if Spring throws an error the handleError method is not called ever?
Thank you.
Seems as if your GlobalDefaultExceptionHandler is catching every Exception upfront. That's why handleError is never called.
Your other endpoints work, cause you are calling them directly - as you describe it.
I would recommend using #ControllerAdvice to handle specific Exceptions an let your CustomErrorController implementation handle all not already handled Exceptions. Spring boot will wrap them inside NestedServletException with Http Status 500. You can get the root cause inside handleError with:
Object exception = request.getAttribute("javax.servlet.error.exception");
if (String.valueOf(exception) != null) {
log.info("Nested Exception: " + String.valueOf(exception));
}
Check those answers for further information on ordering and the error work flow in spring boot:
order
spring boot error handling flow

spring boot unit testing using testng

How to write a POST method test case if the return type of a particular create method in the service layer is ResponseEntity<Object>?
This is my createOffer method:
public ResponseEntity<Object> createOffer(Offer offer) {
Offer uoffer = offerRepository.save(offer);
URI location = ServletUriComponentsBuilder.fromCurrentRequest().path("/{jobTitle}").
buildAndExpand(uoffer.getJobTitle()).toUri();
return ResponseEntity.created(location).build();
}
and this is its corresponding test class method:
#Test
public void testCreateOffer() {
Offer offer = new Offer("SE",new Date(),5);
Mockito.when( offerRepository.save(offer)).thenReturn( offer);
assertThat(offerServiceImpl.createOffer(offer)).isEqualTo(offer);
}
Here I am getting an error while running this test case which is no current servlet request attributes and exception is:
java.lang.IllegalStateException
Why is it coming
This answers the above question.
Hope it helps when someone finds the same issue !!!
#Test
public void testCreateOffer() {
Offer offer = new Offer("SE",new Date(),5);
MockHttpServletRequest request = new MockHttpServletRequest();
RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request));
URI location = ServletUriComponentsBuilder.fromCurrentRequest().path("/{jobTitle}").
buildAndExpand(offer.getJobTitle()).toUri();
ResponseEntity<Object> response = ResponseEntity.created(location).build();
Mockito.when( offerRepository.save(offer)).thenReturn(offer);
assertThat( offerServiceImpl.createOffer(offer)).isEqualTo(response);
}
Problem is that in your method you want to get infromation from class ServletUriComponentsBuilder. When you open this class in comment is
UriComponentsBuilder with additional static factory methods to create
links based on the current HttpServletRequest.
So it means when your application is running on server (e.g. tomcat) you have context and you can read information from HttpServletRequest. But in junit you don't have context and you can't get this iformation. So when your code is runnig and reach the ServletUriComponentsBuilder.fromCurrentRequest() then the code is done. So you have to mock it. Look at this link it can help you.
ServletUriComponentsBuilderTests
Kotlin.
I was getting java.lang.IllegalStateException: No current ServletRequestAttributes cause I had this line in my service:
val location = ServletUriComponentsBuilder.fromCurrentRequest().build().toUri()
I have put the following into my setUp() function:
#BeforeEach
fun setup() {
MockitoAnnotations.openMocks(this)
val request = MockHttpServletRequest()
RequestContextHolder.setRequestAttributes(ServletRequestAttributes(request))
}

Spring cloud contract best practice to handle unhappy path

We used to have wiremock for integration tests with both happy and unhappy paths. Now we are trying to move to Spring cloud contract based integration tests. Though, I could not find any document related to contracts for unhappy paths (status code over 400). And I did some POC with status code 4xx/5xx in response but it didnot work.
Anyone knows the best practice to handle unhappy paths in consumer side? or is it not supported at all for unhappy paths with status code over 400 with spring cloud contract?
Here is an example:
Producer side
Contract.make {
description 'get 404 when entity was not found'
request {
method GET()
url '/entities/0'
}
response {
status NOT_FOUND()
}
}
Client side
#RunWith(SpringRunner.class)
#SpringBootTest(classes = SomeApplication.class)
#AutoConfigureStubRunner(ids = "io.app:entity:+:stubs:8080")
#AutoConfigureTestDatabase
public class EntityClientTest {
#Rule
public ExpectedException exception = ExpectedException.none();
#Autowired
private EntityClient entityClient; // This is a FeignClient
#Test
public void shouldThrowNotFoundWithInvalidId() {
exception.expect(FeignException.class);
exception.expectMessage("404");
entityClient.getById(0);
}
}
As you can see, the getById thrown a 404 because the contract says so.

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);
} }

Resources