Skip to main content

Async Assertions

When the action under test completes asynchronously — a message lands on a queue, a status flips after a callback — a plain then() races the system. The assertion mixins provide two polling variants:

MethodSemantics
thenEventually(...) / andEventually(...)Re-runs the collector + assertion until it passes, or the timeout expires (fails with the last assertion error)
thenContinually(...)Re-runs the collector + assertion for the whole duration and fails the moment it stops passing — "this must keep being true"

Both render in the report as first-class sentence keywords (Then eventually …), and every assertion mixin (WithKotest, WithAssertJ, WithHamcrest, WithHamkrest) provides them.

Defaults: 10-second timeout, 25 ms poll interval, no initial delay.

Kotest

import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds

@Test
fun `order is eventually confirmed`() {
whenever { orderService.placeOrder() }

// polls the collector until the block passes (default: 10s timeout, 25ms interval)
thenEventually(theOrderStatus()) { this shouldBe "CONFIRMED" }
}

@Test
fun `order is eventually confirmed with custom timeout`() {
whenever { orderService.placeOrder() }

thenEventually(30.seconds, theOrderStatus()) { this shouldBe "CONFIRMED" }
andEventually(theOrderReference(), startWith("ORD-"))
}

Timing is fully tunable via named parameters:

thenEventually(
initialDelay = 500.milliseconds,
duration = 30.seconds,
interval = 100.milliseconds,
collector = theOrderStatus(),
match = startWith("CONF")
)

And the inverse — assert a condition keeps holding:

@Test
fun `order is never cancelled`() {
whenever { orderService.placeOrder() }

// asserts the condition holds for the whole duration
thenContinually(5.seconds, theOrderStatus()) { this shouldBe "CONFIRMED" }
}

Both accept either a matcher (Matcher<T>) or an assertion block (T.() -> Unit), just like then().

AssertJ (Java)

WithAssertJ provides thenEventually with the same collector-plus-assertion shape as its then:

import java.time.temporal.ChronoUnit;

@Test
void orderIsEventuallyConfirmed() {
whenever(ctx -> orderService.placeOrder());

// polls the collector until the assertion passes (default: 10 seconds)
thenEventually(theOrderStatus(), status -> assertThat(status).isEqualTo("CONFIRMED"));
}

@Test
void orderIsEventuallyConfirmedWithCustomTimeout() {
whenever(ctx -> orderService.placeOrder());

thenEventually(30L, ChronoUnit.SECONDS, theOrderStatus(),
status -> assertThat(status).isEqualTo("CONFIRMED"));
}

Hamcrest / HamKrest

WithHamcrest and WithHamkrest provide thenEventually and thenContinually taking their respective Matcher<T> types, with the same optional leading Duration:

thenEventually(theOrderStatus(), equalTo("CONFIRMED"))
thenContinually(5.seconds, theOrderStatus(), equalTo("CONFIRMED"))

In the report

The keywords are recognised by the sentence renderer, so

thenEventually(theOrderStatus()) { this shouldBe "CONFIRMED" }

renders as Then eventually the order status … — the report reads as the requirement ("the order is eventually confirmed") rather than as polling mechanics.