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"));

Hourglass on Brown Wooden Frame

Photo by Mike

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.