Unit Testing Circuit Breaker
with Resilience4J, Spring Boot, Java
As Microservices architecture is gaining more and more adoption (sometimes well justified, sometimes overused) many accompanying technologies and best practices, such as Circuit Breakers, come along.
As always, it’s important to understand what the technology is designed for, and equally as important what it is not for. There’s a lot of theory that describes what Circuit Breakers are intended for and how they work, but when it comes to practice, sometimes there seems to be a gap between theory and implementation.
As a strong believer in automated testing, I trust that test-driven mindset helps to understand the intention of the technology and to ensure that it’s being used in the right way.
So let’s look at Circuit Breakers from the Unit Testing perspective to clarify the understanding and ensure that your system is working with Circuit Breakers as intended. And make sure of it on each build with automated testing, giving freedom and courage for refactoring and further improvements.
Circuit Breakers recap
Let’s consider this simplified, but rather common use case:
At the system level we have:
- Microservice — our RESTful or another service that uses external independent upstream systems for its functionality: e.g. a DB, asynchronous communication mechanism (JMS, Kafka, MQ), or another external Backend Service;
- External Client — external clients calling our Microservice;
- Upstream Systems — Backend Service, DB, Messaging — external upstream systems that the Microservice uses/depends on for its full functionality.
For clarity and further references let’s define the main components within the Microservice:
- Service — the main point of our interest that encapsulates communication with external system(s), that is where Circuit Breaker is being applied;
- Service Client — internal component of Microservice (e.g. a Controller) that calls our Service;
- Service Interface — explicit or implicit part of the Service and the only part of the Service, that Service Client should “know about”
(“knows about” is one of the few most important considerations during systems design); - Service Implementation — the part of the Service that knows all about and encapsulates interaction with Backend Systems. In the case of a DB, it would be quite different from interacting with asynchronous messaging or a RESTful service. The difference is not only in the way how they are implemented but also in how errors should be treated. These specifics (depending on functional and non-functional requirements) might influence how Circuit Breaker should be applied and configured.
Circuit Breakers are…
Circuit Breakers are applicable in situations when Upstream Systems can become unavailable, unstable, or slow (can’t they all?) and, as a result, effect
- our application by draining resources (e.g. blocking threads, depleting thread pools, using extensive memory) and/or introducing delays or timeouts to our clients (e.g. breaking SLAs);
- any downstream systems depending on ours, also creating a chained effect of resource-draining in each system involved in the call chain.
In such cases, Circuit Breaker can accumulate statistics of external calls and “decide” to stop calling Upstream Systems and failing right away instead. This saves time for the calls that are expected to fail eventually anyway and saves all the related resources used in our system and the whole downstream call chain.
Circuit Breakers are not…
Circuit Breakers are not helping Upstream Systems to become more resilient in any way (instead they help making our and other Downstream Systems more resilient).
Circuit Breakers are not error handling or error prevention mechanisms; and they are not retry mechanisms.
Circuit Breakers work based on statistical data which comes at a price that Circuit Breaker could be in an “open” state and prevent calling Upstream System that could have been successfully called otherwise.
Circuit Breakers can’t differentiate by themselves between different kinds of error situations reported by Upstream Systems. Some of them can be connectivity, slowness, or stability issues, while the others can report business errors (e.g. as per requirements). It’s important to implement Circuit Breakers diligently in such cases — not mix business errors into common error statistics causing “false positive” behavior and “opening” Circuit Breakers when they should not.
Let’s put Circuit Breaker into the picture
In applications using Spring Boot, Resilience4J Circuit Breaker is added by just one simple annotation on a method of Service Implementation. But don’t get confused with simplicity — semantics needs to be understood well to use Circuit Breaker properly. In practice, Spring will create a Proxy for Service Implementation and inject it into all Service Clients using Service Interface. That Proxy will contain the Circuit Breaker logic that will either pass through the call to Service Implementation when it’s “closed”, or not if it’s been tripped “open”.
Test your Circuit Breakers
The main value of the test-driven development approach is that you think of the test scenarios and their outcomes before you code your implementation. Coding your test before your implementation is very beneficial in many ways as well, but the former is vital.
Let’s outline the scenarios:
- Circuit Breaker “closed” and Backend Service successfully responding and in a timely manner;
- Circuit Breaker “closed”, Backend Service fails and Service Client handles the scenario as expected;
- Circuit Breaker “open” and Service Client handles the scenario as expected;
- Testing Circuit Breaker state transitions according to configuration. This I find more in the area of systems integration test and it will not be included in this article.
Testing scope and system under test
For the purpose of this article, I’ll stick to the example above where the Service is called by a Controller acting as a Service Client and Service Implementation is encapsulating communication with a database using Spring Data.
The system under test is Service Implementation with Circuit Breaker injected in between itself and Service Client.
In Spring Boot applications that are using Resilience4J, Circuit Breakers are injected as an aspect between Service Implementation and its clients by Spring’s dependency injection according to Spring Boot’s autoconfiguration. Spring Boot testing harness will be used to instantiate the system under test. It will set up Application Context in the same way as in production and inject the Circuit Breaker, but it will also allow injecting Mocks of underlying components. Components from the data layer will be mocked and injected into Spring Application Context to influence the expected behavior of the Circuit Breaker and its effect on the Service Client.
The code for the test cases below is rather concise, but the most important parts are which components of the system are being mocked and set into expected state, and which are real and under test.
To be continued
I hope to continue with the next chapter on practical Circuit Breaker implementation called Circuit Breaker — Common pitfalls.
- Hiding exceptions from Circuit Breaker by catching them in Service Implementation
- Misusing Fallback method
- Different circuit breaker instances for one resource