As a company that provides a high standard in user experiences, it is common to use an asynchronous system in Halodoc. With an application that heavily relies on the backend, an asynchronous system is preferable to be used as it enables seamless experiences to the user by improving application performance and responsiveness.
In this blog, we will outline how we leverage Awaitility to enabling asynchronous integration testing of Microservice.
One example of an asynchronous process in Halodoc is when users try to order medicine from a pharmacy, few fulfillment processes need to be handled.
As we can see from the image above, validation processes are done to determine whether an order can be fulfilled or not and by whom. With this approach, if we do not use an asynchronous process, users will need to wait for this process to finish before they can continue to do other activities. Instead, you will receive an immediate response which will allow you to continue browsing through other pages or products.
In terms of testing, a quality engineer must validate complete business process flows which commonly consist of both synchronous and asynchronous systems. While many approaches can be used to test synchronous systems, testing an asynchronous system proves to be challenging.
What is Awaitility?
Awaitility comes to overcome this challenge. It is a java domain-specific language that is created specifically to validate asynchronous system processes. Few reasons why Awaitiliy is a great tool for asynchronous system testing are:
- Awaitility is a domain-specific language where we can express our expectations from the system in an easy-to-read manner.
- Awaitility is easy to implement by hiding multithreading complexity where we can control timing parameters easily for our condition checking by using polling features.
How To Setup
1. Add Dependencies
We only need to add Awaitility dependencies in our pom.xml file which will cover most of our test cases.
2. Setup Default Timing Parameters
Awaitility provides an easy way for us to implement timing parameters that can be used across all await statements. By having this approach, we will not be bothered with handling multithreading complexity by putting Thread.sleep() across our test cases. There are three methods that we used in Halodoc to set timing parameters that will be applicable across all await statements.
As you can see in the image above, we can easily define these methods in the beforeSuite method, so that it can be used across all our test cases. The function of these methods are as follows:
Awaitility.setDefaultPollDelay(long pollDelay, Timeunit unit)
This method is used to set an initial poll delay for all await statement by default so that first-time condition checking is done after the delay period is met. It will be useful to apply this method if we know that at least a certain period amount of time is needed before the expected result is generated by our asynchronous system.
Awaitility.setDefaultPollInterval(long pollInterval, Timeunit unit)
This method is used to set the default interval between each condition checking. When determining poll interval, it is important to consider the amount of load that our system will take to process condition checking. The shorter we put the interval, the bigger a load of our system will take, except if we are lucky enough that during a few checking trials, the expected result is met.
Awaitility.setDefaultTimeout(long timeout, Timeunit unit)
This method is used to set the default timeout for all condition-checking processes. If the timeout is reached, the condition checking method will return false indicating that the expected result is not achieved.
3. Implement Condition Checking
In Halodoc, we provide an easy and convenince way to connect our customers (who intend to order medicines) with the pharmacies through our an order processing system, that emcompasses order checkout and delivery tracking workflows. With this approach, we have a complex fulfillment system to connect customers to thousands of pharmacies available in Indonesia.
To provide seamless user experiences, we use an asynchronous system in an order processing system in Halodoc. It is easier to test the system by implementing condition checking with Awaitility. The simplest way is to call the method await.until(Callable<Boolean> conditionEvaluator). Here are the steps to implement condition checking in Awaitility:
- Import following methods statically in your test helper class
- As we can see in the above explanations, the condition checking method needs a callable condition evaluator method to check our expected result. What we need to do is to implement the callable method based on our needs.
- In this case, because Halodoc using an asynchronous system to process a customer's order, we check expected status periodically based on Awaitility timing parameters setting to wait until expected status occurs before continuing to the next test steps. The test case looks like the following image.
4. Using Assertion with Condition Checking
In Halodoc, we use the assertion method instead of putting await method directly in the test case to check whether the expected condition is achieved or not.
To limit the scope of the method defined, we modify the condition evaluator method from public method to private
Then, we can create a public condition checking method that calls the above condition evaluator method
Finally, we can put assertion in our test case to check whether the expected result is achieved or not.
By using this approach, if the condition checking is not achieved until the timeout period, the test case will be treated as failed test case instead of an error test case.
Adding Redis Checking to Check Locking
Additionally, when a few microservices use shared resources, we can add check Redis locking before entering our condition checking scenario, to ensure that shared resources are already free.
First, we need to initialize our locking object. In this case, we can use the Redisson library
Then modify our condition checking method to check locking for shared resources before proceeding to check the condition scenario
And condition evaluator method for lock checking can be designed as following
Code above is implemented to check Redis locking where, in this case, we are using order id as key for our shared resource. After we ensure that lock for the shared resource is already free, we can proceed with our test case scenario. By having this implementation, we can shorten the processing time for our testing because we already make sure that we can proceed to our condition checking that uses shared resources as we already ensure that the shared resource is available.
This article explains how we can overcome challenges in testing asynchronous systems using Awaitility by providing timing parameters and condition checking methods in an easy-to-read manner so that we can easily achieve end-to-end testing with an asynchronous system involved.
Scalability, reliability and maintainability are the three pillars that govern what we build at Halodoc Tech. We are actively looking for engineers at all levels and if solving hard problems with challenging requirements is your forte, please reach out to us with your resumé at firstname.lastname@example.org.
Halodoc is the number 1 all around Healthcare application in Indonesia. Our mission is to simplify and bring quality healthcare across Indonesia, from Sabang to Merauke. We connect 20,000+ doctors with patients in need through our Tele-consultation service. We partner with 3500+ pharmacies in 100+ cities to bring medicine to your doorstep. We've also partnered with Indonesia's largest lab provider to provide lab home services, and to top it off we have recently launched a premium appointment service that partners with 500+ hospitals that allow patients to book a doctor appointment inside our application. We are extremely fortunate to be trusted by our investors, such as the Bill & Melinda Gates Foundation, Singtel, UOB Ventures, Allianz, GoJek, Astra, Temasek and many more. We recently closed our Series C round and In total have raised around USD$180 million for our mission. Our team works tirelessly to make sure that we create the best healthcare solution personalised for all of our patient's needs, and are continuously on a path to simplify healthcare for Indonesia.