Blog

Testing Exception Handling of Spring's REST Controllers

Testing Exception Handling of Spring's REST Controllers

Testing Exception Handling of Spring's REST Controllers

Spring
Development
Spring
Development

17.12.2021

by

-

6 min

read

This blog post gives you an effortless way to test whether your exception handling of Spring Boot's RestTemplate is working. In order to do so we leverage Chaos Engineering and this way can prevent the unattractive path of mocking a part of your system or cumbersome manual testing effort.

Spring Boot’s RestTemplate

Microservices and distributed applications are a rising trend in today’s software development practices. One thing for sure is that at some point two microservices have the need to communicate with each other. The simplest approach to achieve that is to use HTTP-based communication like REST (Representational State Transfer). To do that, we use in Spring Boot the RestTemplate as shown in the Listing below. In this example we reach an endpoint given as parameter (url) via HTTP GET and expect a list of type Product in the response body. This response is used in an orchestrated endpoint reachable via /products/ and collects various products from endpoints of different microservices (more context of the demo online shop can be found here).

@RestController
@RequestMapping("/products")
public class ProductsController {
    @Value("${rest.endpoint.hotdeals}")
    private String urlHotDeals;
    ...

    @GetMapping
    public Products getProducts() {
        Products products = new Products();
        products.setFashion(getProduct(urlFashion));
        products.setToys(getProduct(urlToys));
        products.setHotDeals(getProduct(urlHotDeals));
        return products;
    }

    private List<Product> getProduct(String url) {
        return restTemplate.exchange(
            url,
            HttpMethod.GET,
            null,
            productListTypeReference
        ).getBody();
    }
}

Well, obviously, this code is very simplified and in reality, we also need to think about the error case. What if the other system is not available? Or answers slowly? What if the response is different than expected? Or erroneous? How to verify that without changing the other system's source code? Right now, the code will throw an (unhandled) exception. To be specific, a HttpClientErrorException in case of HTTP 4xx responses and a HttpServerErrorException in case of HTTP 5xx responses. In order to handle the exception appropriately, Spring Boot offers multiple ways - as shown e.g. in the blog post of Baeldung about "Spring RestTemplate Error Handling".

Tests first, keep Code as it is

So, while the above-described solution works, we haven't considered the error handling for now before improving that we follow the paradigm of Test Driven Development of tests-first and would like to test it for real - without mocking or temporary code changes. Luckily, Steadybit can change the behavior of a Java application at runtime via bytecode injection, without re-deploying anything and without the requirement of additional source code dependencies. Once the agents are installed, Steadybit has covered the application as visible on our dashboard (see Figure below), and we can start with our first tests on unhandled exceptions.

As Steadybit has already discovered our system, we can easily create a few experiments to check on the behavior. We start with testing what happens if one of the called endpoints (e.g. hotdeals at ${urlHotDeals}/products) throws an exception.


Step 1: Create an Application Experiment

The first thing to do is to create and define a new experiment to provoke an erroneous endpoint. When using steadybit, this is pretty straightforward:

  1. We go to Experiments and choose to create a new Application Experiment (because we are injecting chaos on the level of the Java Virtual Machine).

  2. We give the experiment a useful name (e.g., "Gateway can handle Hot-Deals exceptions") and choose the Global area for now.

  3. We choose to attack the hotdeals application, as shown below.

  1. Since we want a maximum effect we choose to have an impact of 100% at the following wizard step.

  2. We apply the "controller exception" attack at the next step to provoke an exception at GET-requests on the /products-endpoint of hotdeals (see Figure below).

Finally, we can use the HTTP Status check of the monitoring section to verify that the Gateway's endpoint of http://k8s.demo.steadybit.io/products always responds with an HTTP OK status within a timeout of 3 seconds (see Figure below). This is our desired behavior of always having products at the gateway even when one product-microservices fails. Create the experiment by finalizing the wizard via "save".

Step 2: Run the Experiment to Check the Behavior

Now it's time to verify the status quo. By starting the experiment, we can see what happens without the need to change anything in the source code or shut down parts of the system (in this case, hotdeals).

Well, okay, that was expected. The exception injected by Steadybit in Hot-Deals leads to an erroneous response of the RestController-endpoint (see logs below). This affects Gateway's current implementation by returning an HTTP 500 (see Figure above). So, by injecting an exception at component 1 (Hot-Deals) and stopping it from working, we also caused an exception at the dependent component 2 (Gateway).

2021-06-15 17:35:19.389 ERROR Request processing failed; nested exception
is: java.lang.RuntimeException: Exception injected by steadybit at com.
steadybit.HotDealsRestController.getHotDeals(HotDealsRestController.java:28)
...
021-06-15 17:35:55.891 ERROR 500 Server Error for HTTP GET "/products"
org.springframework.web.client.HttpServerErrorException$InternalServerError: 500 : [{"timestamp":"2021-06
15T17:35:55.888+00:00","status"

Improve Implementation

Our goal is to fix the current implementation by adding proper exception handling and validate it via the created experiment afterward. There are two general places to work on the issue: The first one is to fix the code at Hot-Deals being able to provide a fallback instead of an HTTP 500, and the second one is to fix the code at Gateway being able to work with erroneous product-responses and provide a proper fallback. We decide to fix it in the Gateway by applying the simplest solution possible: a try-catch-block, as seen in the Listing below.

private List<Product> getProduct(String url) {
    try {
        return restTemplate.exchange(
            url,
            HttpMethod.GET,
            null,
            productListTypeReference
        ).getBody();
    } catch (RestClientException e) {
        log.error("RestClientException while fetching products", e);
        return Collections.emptyList();
    }
}


Validate Improvement

Now that we have improved the implementation, we need to check whether it works as expected. For that, we reuse the existing experiment and re-execute it. This time the experiment is successful since the /product-endpoint always returns an HTTP OK with a valid JSON. When hotdeals is erroneous, an empty list replaces it’s content.

Conclusion

In this blog post, we tested and improved the error handling for a synchronous HTTP request. By using Steadybit, we could verify the different error states without changing any source code or shutting down parts of the system. However, the current implementation is still very simplistic. In a real-world use case, you may need a more advanced solution, for instance, a circuit breaker. Thereby, the gateway component reduces the number of requests forwarded to hotdeals and, thus, the load on hotdeals. This is especially desirable when the reason of hotdeals' exception is related to increased load. Check out the implementation of that endpoint in GitHub and verify it with the created experiment.

Are we safe now?

The short answer to this question is: no. There is more to test, and we will cover these in future blog posts. Stay tuned!