1. Introduction
A common problem with asynchronous systems is that it’s hard to write readable tests for them that are focused on business logic and are not polluted with synchronizations, timeouts, and concurrency control.
In this article, we are going to take a look at Awaitility — a library which provides a simple domain-specific language (DSL) for asynchronous systems testing.
With Awaitility, we can express our expectations from the system in an easy-to-read DSL.
2. Dependencies
We need to add Awaitility dependencies to our pom.xml.
The awaitility library will be sufficient for most use cases. In case we want to use proxy-based conditions, we also need to provide the awaitility-proxy library:
<dependency> <groupId>org.awaitility</groupId> <artifactId>awaitility</artifactId> <version>3.0.0</version> <scope>test</scope> </dependency> <dependency> <groupId>org.awaitility</groupId> <artifactId>awaitility-proxy</artifactId> <version>3.0.0</version> <scope>test</scope> </dependency>
You can find the latest version of the awaitility and awaitility-proxy libraries on Maven Central.
3. Creating an Asynchronous Service
Let’s write a simple asynchronous service and test it:
public class AsyncService { private final int DELAY = 1000; private final int INIT_DELAY = 2000; private AtomicLong value = new AtomicLong(0); private Executor executor = Executors.newFixedThreadPool(4); private volatile boolean initialized = false; void initialize() { executor.execute(() -> { sleep(INIT_DELAY); initialized = true; }); } boolean isInitialized() { return initialized; } void addValue(long val) { throwIfNotInitialized(); executor.execute(() -> { sleep(DELAY); value.addAndGet(val); }); } public long getValue() { throwIfNotInitialized(); return value.longValue(); } private void sleep(int delay) { try { Thread.sleep(delay); } catch (InterruptedException e) { } } private void throwIfNotInitialized() { if (!initialized) { throw new IllegalStateException("Service is not initialized"); } } }
4. Testing with Awaitility
Now, let’s create the test class:
public class AsyncServiceTest { private AsyncService asyncService; @Before public void setUp() { asyncService = new AsyncService(); } //... }
Our test checks whether initialization of our service occurs within a specified timeout period (default 10s) after calling the initialize method.
This test case merely waits for the service initialization state to change or throws a ConditionTimeoutException if the state change does not occur.
The status is obtained by a Callable that polls our service at defined intervals (100ms default) after a specified initial delay (default 100ms). Here we are using the default settings for the timeout, interval, and delay:
asyncService.initialize(); await() .until(asyncService::isInitialized);
Here, we use await — one of the static methods of the Awaitility class. It returns an instance of a ConditionFactory class. We can also use other methods like given for the sake of increasing readability.
The default timing parameters can be changed using static methods from the Awaitility class:
Awaitility.setDefaultPollInterval(10, TimeUnit.MILLISECONDS); Awaitility.setDefaultPollDelay(Duration.ZERO); Awaitility.setDefaultTimeout(Duration.ONE_MINUTE);
Here we can see the use of the Duration class, which provides useful constants for the most frequently used time periods.
We can also provide custom timing values for each await call. Here we expect that initialization will occur at most after five seconds and at least after 100ms with polling intervals of 100ms:
asyncService.initialize(); await() .atLeast(Duration.ONE_HUNDRED_MILLISECONDS) .atMost(Duration.FIVE_SECONDS) .with() .pollInterval(Duration.ONE_HUNDRED_MILLISECONDS) .until(asyncService::isInitialized);
It’s worth mentioning that the ConditionFactory contains additional methods like with, then, and, given. These methods don’t do anything and just return this, but they could be useful to enhance the readability of test conditions.
5. Using Matchers
Awaitility also allows the use of hamcrest matchers to check the result of an expression. For example, we can check that our long value is changed as expected after calling the addValue method:
asyncService.initialize(); await() .until(asyncService::isInitialized); long value = 5; asyncService.addValue(value); await() .until(asyncService::getValue, equalTo(value));
Note that in this example, we used the first await call to wait until the service is initialized. Otherwise, the getValue method would throw an IllegalStateException.
6. Ignoring Exceptions
Sometimes, we have a situation where a method throws an exception before an asynchronous job is done. In our service, it can be a call to the getValue method before the service is initialized.
Awaitility provides the possibility of ignoring this exception without failing a test.
For example, let’s check that the getValue result is equal to zero right after initialization, ignoring IllegalStateException:
asyncService.initialize(); given().ignoreException(IllegalStateException.class) .await().atMost(Duration.FIVE_SECONDS) .atLeast(Duration.FIVE_HUNDRED_MILLISECONDS) .until(asyncService::getValue, equalTo(0L));
7. Using Proxy
As described in section 2, we need to include awaitility-proxy to use proxy-based conditions. The idea of proxying is to provide real method calls for conditions without implementation of a Callable or lambda expression.
Let’s use the AwaitilityClassProxy.to static method to check that AsyncService is initialized:
asyncService.initialize(); await() .untilCall(to(asyncService).isInitialized(), equalTo(true));
8. Accessing Fields
Awaitility can even access private fields to perform assertions on them. In the following example, we can see another way to get the initialization status of our service:
asyncService.initialize(); await() .until(fieldIn(asyncService) .ofType(boolean.class) .andWithName("initialized"), equalTo(true));
9. Conclusion
In this quick tutorial, we introduced the Awaitility library, got acquainted with its basic DSL for the testing of asynchronous systems, and saw some advanced features which make the library flexible and easy to use in real projects.
As always, all code examples are available on Github.