Why doesnt Springboot Mockmvc.perform handle exceptions on controller? - spring-boot

I have written the following code and the mockmvc.perform does not catch exception instead returns an error stack. I used the debugger to confirm that the controller throws the correct error. I am new to SpringBoot and do not understand why the exception is not being handled by the test controller.The following is my test controller which makes three Api calls to external services.The exception is returned by the controller but Mockmvc.perform fails to assert it.
#ExtendWith(SpringExtension.class)
#SpringBootTest(classes = { Application.class, ApplicationTest.class })
#AutoConfigureMockMvc
#ContextConfiguration(initializers = {WireMockInitializer.class})
public class myControllerIntegrationTest {
#Autowired
private MockMvc mockMvc;
#Autowired private WireMockServer wireMockServer;
#Autowired
private myController myController;
#Before
public void setup() {
this.mockMvc = MockMvcBuilders.standaloneSetup(myController)
.build();
}
#Test
#DisplayName("Should Return Execution Error")
public void shouldReturnExecutionErrorOnService() throws Exception {
// Making Three Api calls the controller internally invokes them
configureStubA(HttpStatus.INTERNAL_SERVER_ERROR, args, "invalidResponse.json");
configureStubB(HttpStatus.INTERNAL_SERVER_ERROR, args, args2, args3,
"invalidResponse.json");
configureStubC(HttpStatus.INTERNAL_SERVER_ERROR, args, "invalidResponse.json");
mockMvc
.perform(
get("/something")
.param("a", a)
.param("b", b)
.param("c", c)
.param("d", d)
.param("e", e.toArray(new String[] {})))
.andDo(print())
.andExpect(status().is5xxServerError())
.andExpect(result -> assertTrue(result.getResolvedException() instanceof
IllegalStateException));
}}

The API itself will never return an exception.
Think of it as when you call an API, you'll always get a response, right?
The way Spring Controller handle an Exception is that it has a default exception handler, that will convert any exception threw from your Controller into a Response object, which will then be converted to json/xml and return to you.

One way to get what you are expecting is by declaring your own exception, annotate it with #ResponseStatus, and pass to it the http status code you want the exception to be mapped to.
For example (I'm using Kotlin in the following snippets) you can declare the mapping between the http status code 500 and your exception in this way:
#ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
class MyException(message: String) : RuntimeException(message)
Then in your test you will be able to assert on the internal error as you are doing in your snippet.
mockMvc.perform(get("/foo")).andExpect(status().isInternalServerError)
For more details take a look here and here

Related

Testing a camel route that calls an http server

In my camel context I have a route defined as follows:
from("direct:getPets")
.routeId("getPets")
.to("http://localhost:4321/pets")
The route works well and makes a call to the server. I'd like to test this route and check the headers but I have issues.
This is a spring project so my test class looks as follows:
#RunWith(CamelSpringBootRunner.class)
#DirtiesContext(classMode = DirtiesContext.ClassMode.After_EACH_TEST_METHO)
#UseAdviceWith
public class TestRestEndpoint {
#Autowired
private CamelContext camelContext;
#Produce(uri = "direct:getPets")
private ProducerTemplate callServer;
#EndpointInject(uri = "mock:catchTestEndpoint")
#Test
public void should_return_json() throws Exception {
camelContext.getRouteDefinitions("getPets").adviceWith(camelContext, new AdviceWithRouteBuilder() {
#Override
public void configure() throws Exception {
weaveAddLast().to("mock:catchTestEndpoint");
}
});
camelContext.start();
callServer.sendBody("");
mockEndpoint.expectedMessageCount(1);
mockEndpoint.assertIsSatisfied();
}
}
The exchange fails and returns a null pointer.
org.apache.camel.CamelExecutionException: Exception occurred during execution on the exchange: Exchange[ID-PVJ-DEV97-03-2-1600178584697-0-1]
Sounds like your appended Mock is null.
If the code in your question really is your code, this is probably because you have a typo in your endpoint URI.
Notice the extra t between "catch" and "Test" in mock:catchtTestEndpoint

How to test exceptions handling in #ControllerAdvice

I currently have two ControllerAdvice in my application, I'm supposed to merge them into one.
But I need to test them before and after the merge, test the exception and the object that the controller return me.
I'm trying to make a jUnit test with Mockito but it seems impossible to test the exceptions without any context, without a controller, etc ...
Does anyone know how can I proceed to achieve what I'm trying to do ?
I also try to throw manually an exception but obviously it wasn't catched by the ControllerAdvice.
So basically here is what i'm trying to do:
Manually throw an exception
This exception is handled by my ControllerAdvice
Check the returned object (code & message)
Here is a sample of code I have:
#Before
public void setup() {
...
mockMvc = MockMvcBuilders.standaloneSetup(getController())
.setControllerAdvice(new GlobalControllerExceptionHandler())
.setCustomArgumentResolvers(resolver, resolver_0, resolver_1)
.setHandlerExceptionResolvers(exceptionResolver).build();
}
#Controller
#RequestMapping("/tests")
public static class RestProcessingExceptionThrowingController {
#RequestMapping(value = "/exception", method = GET)
public #ResponseBody String find() {
throw new EntityNotFoundException();
}
}
#Test
public void testHandleException() throws Exception {
mockMvc.perform(get("/tests/exception"))
.andExpect(new ResultMatcher() {
#Override
public void match(MvcResult result) throws Exception {
result.getResponse().getContentAsString().contains("global_error_test");
}
})
.andExpect(status().isNotFound());
}
I have the good status code at the end but it doesn't use my ControllerAdvice (I try with the debugger)
You can just call handler method directly
#ControllerAdvice
MyAdvice{
#ExceptionHandeler(listOfExxcetpions)
public ResponseEntity someOfMyExceptionsHandler(Exception e){
.....
}
}
and in test
MuTest{
private MyAdvice advice=new MyAdvice();
#Test
public void oneOfTests(){
Exception e=new SomeSortOfExceptionToTest();
resp=advice.someOfMyExceptionsHandler(e)
assertThat(resp).....dostuff;
}
}
If you want to test how spring integrates with your handlers - if your annotations are correct, ordering serialization etc - well that will be an integration test and you have to boot up test context - then you can throw exceptions directly from controller methods.

How to prevent NestedServletException when testing Spring endpoints?

I am trying to test the security configuration of some of my endpoints which are secured with #PreAuthorize(#oauth2.hasScope('scope'). When accessing such an endpoint via Postman with a access token that does not have the required scope, the following is returned with HTTP status code 403 (forbidden):
{
"error": "insufficient_scope",
"error_description": "Insufficient scope for this resource",
"scope": "scope"
}
Which is the expected behaviour that I want.
When trying to test this configuration, Springs NestedServletException interferes with my test case before it can complete with my expected result.
This is a simplified version of the controller I want to test:
#RestController
#RequestMapping(value = "/api")
public class OauthTestingResource {
#PreAuthorize(#oauth2.hasScope('scope'))
#RequestMapping(value = "/scope", method = RequestMethod.GET)
public void endpoint() {
// ...
}
}
And this is the corresponding test case:
#RunWith(SpringJUnit4ClassRunner.class)
#SpringBootTest(classes = MyApplication.class)
#WebAppConfiguration
public class AuthorizationTest {
#Autowired
protected WebApplicationContext webApplicationContext;
protected SecurityContext securityContext = Mockito.mock(SecurityContext.class);
#Before
public void setup() throws Exception {
this.mvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
SecurityContextHolder.setContext(securityContext);
}
protected Authentication createMockAuth(Client client) {
final List<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
final Authentication pwAuth = new UsernamePasswordAuthenticationToken("testuser", "testpw", authorities);
final TokenRequest request = new TokenRequest(new HashMap<>(), client.getClientId(), client.getScopes(), "password");
final OAuthClient oauthClient = new OAuthClient(client, GrantType.PASSWORD);
return new OAuth2Authentication(request.createOAuth2Request(oauthClient), pwAuth);
}
#Test
public void testAppScope() throws Exception {
final Client client = new Client("id1", "secret1");
client.setScope("scope");
Mockito.when(securityContext.getAuthentication()).thenReturn(createMockAuth(client));
// this test passes
mvc.perform(get("/api/scope")).andExpect(status().isOk());
client.setScope("other_scope");
Mockito.when(securityContext.getAuthentication()).thenReturn(createMockAuth(client));
// NestedServletException thrown here
mvc.perform(get("/api/scope")).andExpect(status().isForbidden());
}
}
The exception that is thrown is the following (which is expected):
org.springframework.web.util.NestedServletException: Request
processing failed; nested exception is
org.springframework.security.access.AccessDeniedException:
Insufficient scope for this resource
My question is how can I prevent this exception from interfering with my test case?
I did spring security test cases by following this link. Things worked fine except this issue of nesting original exception in NestedServletException.
I did not find any direct way to figure this out but AspectJ helped me in handling this in a cleaner way.
We can use the static assertThatThrownBy() method of the Assertions class. This method returns an AbstractThrowableAssert object that we can use to write assertions for the thrown exception.
The code that captures an exception thrown by the methodThatThrowsException() method looks as follows:
assertThatThrownBy(() -> methodThatThrowsException())
.isExactlyInstanceOf(DuplicateEmailException.class);
Thanks to this excellent blog where you can find additional details.
The way in which I handled this in my test case would be (by taking your test case codeline):
org.assertj.core.api.Assertions.assertThatThrownBy(() -> mvc.perform(get("/api/scope")).andExpect(status().isOk())).hasCause(new AccessDeniedException("Access is denied"));
That way your test case would be able to assert actual AccessDeniedException that is nested in NestedServletException.
I had a similar case and suppressed the NestedServletException by using
#Test(expected = NestedServletException.class) and then I was able to get hold of the MvcResult and do further asserts on it as in other tests like:
// then
MvcResult result = resultActions.andExpect(status().isServiceUnavailable()).andReturn();
String message = result.getResponse().getContentAsString();
assertThat(message).contains("ABC");
assertThat(result.getResolvedException().getClass()).isEqualTo(XYZ.class);
It seemed to work.
I fixed it by adding an #ExceptionHandler for that exception. Seems like if MockMvc throws an actual exception it means you don't "handle" this case which is not ideal.
If you are using a centralised class to do your error handling via AOP (using #ControllerAdvice annotation), then include this ControllerAdvice in your test setup.
#BeforeEach
public void setup() {
this.mockMvc = standaloneSetup(myController).setControllerAdvice(MyErrorHandlingAdvice.class).build();
}

Spring Boot Testing: exception in REST controller

I have a Spring Boot application and want to cover my REST controllers by integration test.
Here is my controller:
#RestController
#RequestMapping("/tools/port-scan")
public class PortScanController {
private final PortScanService service;
public PortScanController(final PortScanService portScanService) {
service = portScanService;
}
#GetMapping("")
public final PortScanInfo getInfo(
#RequestParam("address") final String address,
#RequestParam(name = "port") final int port)
throws InetAddressException, IOException {
return service.scanPort(address, port);
}
}
In one of test cases I want to test that endpoint throws an exception in some circumstances. Here is my test class:
#RunWith(SpringRunner.class)
#WebMvcTest(PortScanController.class)
public class PortScanControllerIT {
#Autowired
private MockMvc mvc;
private static final String PORT_SCAN_URL = "/tools/port-scan";
#Test
public void testLocalAddress() throws Exception {
mvc.perform(get(PORT_SCAN_URL).param("address", "192.168.1.100").param("port", "53")).andExpect(status().isInternalServerError());
}
}
What is the best way to do that? Current implementation doesn't handle InetAddressException which is thrown from PortScanController.getInfo() and when I start test, I receive and error:
org.springframework.web.util.NestedServletException: Request processing failed; nested exception is com.handytools.webapi.exceptions.InetAddressException: Site local IP is not supported
It is not possible to specify expected exception in #Test annotation since original InetAddressException is wrapped with NestedServletException.
Spring Boot Test package comes with AssertJ that has very convenient way of verifying thrown exceptions.
To verify cause:
#Test
public void shouldThrowException() {
assertThatThrownBy(() -> methodThrowingException()).hasCause(InetAddressException .class);
}
There are also few more methods that you may be interested in. I suggest having a look in docs.
In order to test the wrapped exception (i.e., InetAddressException), you can create a JUnit Rule using ExpectedException class and then set the expectMessage() (received from NestedServletException's getMessage(), which contains the actual cause), you can refer the below code for the same:
#Rule
public ExpectedException inetAddressExceptionRule = ExpectedException.none();
#Test
public void testLocalAddress() {
//Set the message exactly as returned by NestedServletException
inetAddressExceptionRule.expectMessage("Request processing failed; nested exception is com.handytools.webapi.exceptions.InetAddressException: Site local IP is not supported");
//or you can check below for actual cause
inetAddressExceptionRule.expectCause(org.hamcrest.Matchers.any(InetAddressException.class))
//code for throwing InetAddressException here (wrapped by Spring's NestedServletException)
}
You can refer the ExpectedException API here:
http://junit.org/junit4/javadoc/4.12/org/junit/rules/ExpectedException.html
You could define an exception handler
#ExceptionHandler(InetAddressException.class)
#ResponseStatus(HttpStatus.BAD_REQUEST)
#ResponseBody
public Response handledInvalidAddressException(InetAddressException e)
{
log e
return getValidationErrorResponse(e);
}
and then in your test you could do
mvc.perform(get(PORT_SCAN_URL)
.param("address", "192.168.1.100")
.param("port", "53"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.response").exists())
.andExpect(jsonPath("$.response.code", is(400)))
.andExpect(jsonPath("$.response.errors[0].message", is("Site local IP is not supported")));
I had the same issue and i fix it with org.assertj.core.api.Assertions.assertThatExceptionOfType :
#Test
public void shouldThrowInetAddressException() {
assertThatExceptionOfType(InetAddressException.class)
.isThrownBy(() -> get(PORT_SCAN_URL).param("address", "192.168.1.100").param("port", "53"));
}
I hope it's help you !

How to call Springs service method from controller (Junit)

I have seen example , how to call spring controller using mockito.
Using Mock I call Spring MVC controller.
Controller Invokes Spring service class.
#RunWith(SpringJUnit4ClassRunner.class)
#WebAppConfiguration
#ContextConfiguration(locations = { "file:src/main/webapp/WEB-INF/spring/root-context.xml" })
public class TestController {
#Mock
private TestService testService;
#InjectMocks
private PaymentTransactionController paymentController;
private MockMvc mockMvc;
#Before
public void setup() {
MockitoAnnotations.initMocks(this);
this.setMockMvc(MockMvcBuilders.standaloneSetup(paymentController).build());
}
#Test
public void test() throws Exception {
this.mockMvc.perform(post("/tr/test").content(...)).andExpect(status().isOk());
// testService.save(); <-- another way
}
Ok it works well. I calls my Spring controller very well. But In Spring controller I have Injected Service Layer.
#Autowired
private TestService serviceTest;
#RequestMapping(value = "/test", method = RequestMethod.POST)
#ResponseBody()
public String test(HttpServletRequest request) {
...
serviceTest.save();
// in save method I call dao and dao perist data;
// I have injected dao intrface in serviceTest layer
...
return result;
}
The problem is that, my app does not invokes save method, it is not entered in it. I have no error too. The same result is when I call save() method from Junit (I have commented it in test() method).
When I debug, I have seen that interrupt method happens of org.mockito.internal.creation.MethodInterceptorFilter
How to solve this problem? what happens?
If you are doing a unit test of your controller, you should mock the service layer (what you are doing). In this kind of test, you just control that :
the correct methods of the controller are triggered and they produce what is expected
the correct methods in service layer are called ... in the mock
You simply have to configure the return values of the methods of the mock (if relevant), or control what was called
#Before
public void setup() {
MockitoAnnotations.initMocks(this);
this.setMockMvc(MockMvcBuilders.standaloneSetup(paymentController).build());
// set return values from the mocked service
when(testService.find(1)).thenReturn(...);
}
and verify later what has been called
#Test
public void test() throws Exception {
this.mockMvc.perform(post("/tr/test").content(...)).andExpect(status().isOk());
// testService.save(); <-- another way
verify(testService, times(1)).save();
}
If you want to do an integration test, you do not mock the service, but setup an application context to inject real beans, but ordinarily use an embedded database instead of the real one.
just change #InjectMocks to #Autowired. This fix the issue! In this case you are not mocking, you are invoking method with real data.
As I understand, you perform post to "/tr/test" resource, but request mapping in your controller is '/payment'. Make sure you post to resource mapped in the controller.

Resources