How to override Awaitility error messages
Awaitility is an excellent Java library. It's especially useful for working with eventually consistent APIs. For instance, when a client sends a POST, PUT, or DELETE request, you might need to make several GET requests before observing the changes, which is terrible for automated tests. A common solution to this problem is to use Awatility. You can ask it to execute code for n
seconds until the request succeeds or the timer expires.
await()
.atMost(Duration.ofSeconds(5))
.until(() -> apiClient().get(id).getStatus().equals("expected"));
Problem
Unfortunately, the default error messages could use some work. Is it clear to you what this code tried to validate and why it failed?
org.awaitility.core.ConditionTimeoutException: Condition with Lambda expression in ee.fakeplastictrees.Await was not fulfilled within 5 seconds.
at org.awaitility.core.ConditionAwaiter.await(ConditionAwaiter.java:167)
at org.awaitility.core.CallableCondition.await(CallableCondition.java:78)
at org.awaitility.core.CallableCondition.await(CallableCondition.java:26)
at org.awaitility.core.ConditionFactory.until(ConditionFactory.java:1160)
at org.awaitility.core.ConditionFactory.until(ConditionFactory.java:1129)
at ee.fakeplastictrees.Await.standard(Await.java:17)
at ee.fakeplastictrees.Main.run(Main.java:14)
Personally, it doesn't tell me much. What's the problem? What condition was Awaitility trying to evaluate? I can't say without looking in the code. Assertj, for instance, allows you to override an error message:
Assertions.assertThat("actual")
.overridingErrorMessage("expected status=%s after purchase notification, but got=%s", expected, actual)
.isEqualTo("expected")
We can do something similar with Awaitility as well.
Solutions
Alias
We can assign an alias for a check.
await()
.alias("expect status=expected after purchase notification")
.atMost(Duration.ofSeconds(5))
.until(() -> apiClient().get(id).getStatus().equals("expected"));
This configuration will produce the following error:
org.awaitility.core.ConditionTimeoutException: Condition with alias 'expect status=expected after purchase notification' didn't complete within 5 seconds because condition with Lambda expression in ee.fakeplastictrees.Await was not fulfilled.
at org.awaitility.core.ConditionAwaiter.await(ConditionAwaiter.java:167)
at org.awaitility.core.CallableCondition.await(CallableCondition.java:78)
at org.awaitility.core.CallableCondition.await(CallableCondition.java:26)
at org.awaitility.core.ConditionFactory.until(ConditionFactory.java:1160)
at org.awaitility.core.ConditionFactory.until(ConditionFactory.java:1129)
at ee.fakeplastictrees.Await.alias(Await.java:24)
at ee.fakeplastictrees.Main.run(Main.java:14)
Much better.
Matcher
We can also use Hamcrest matchers.
await()
.atMost(Duration.ofSeconds(5))
.until(() -> apiClient().get(id).getStatus(),
Matchers.describedAs("status=expected after purchase notification",
equalTo("expected"));
This configuration will produce the following error:
org.awaitility.core.ConditionTimeoutException: Lambda expression in ee.fakeplastictrees.Await expected status=expected after purchase notification but was actual within 5 seconds.
at org.awaitility.core.ConditionAwaiter.await(ConditionAwaiter.java:167)
at org.awaitility.core.AbstractHamcrestCondition.await(AbstractHamcrestCondition.java:86)
at org.awaitility.core.ConditionFactory.until(ConditionFactory.java:1160)
at org.awaitility.core.ConditionFactory.until(ConditionFactory.java:712)
at ee.fakeplastictrees.Await.matcher(Await.java:30)
at ee.fakeplastictrees.Main.run(Main.java:14)
Another solid solution.
Assertj
Finally, if we like Assertj, why not use this library?
await()
.atMost(Duration.ofSeconds(5))
.untilAsserted(() -> Assertions.assertThat(apiClient().get(id).getStatus())
.overridingErrorMessage("expect status=expected after purchase notification")
.isEqualTo("expected"));
This configuration will produce the following error:
Caused by: java.lang.AssertionError: expect status=expected after purchase notification
at ee.fakeplastictrees.Await.lambda$asserted$3(Await.java:38)
at org.awaitility.core.AssertionCondition.lambda$new$0(AssertionCondition.java:53)
at org.awaitility.core.ConditionAwaiter$ConditionPoller.call(ConditionAwaiter.java:248)
at org.awaitility.core.ConditionAwaiter$ConditionPoller.call(ConditionAwaiter.java:235)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
at java.base/java.lang.Thread.run(Thread.java:1583)
Conclusion
I use Assertj extensively in my projects because I appreciate how powerful this library is. Hence, I prefer the latter option. However, all three solutions solve the problem and make stacktraces clearer and easier to understand for developers, who want to know what went wrong.