skip to content

Non-Deterministic Testing (eventually)

eventually, until, and continually poll an assertion over a time window instead of sleeping, turning async outcomes into stable tests. Interviewers ask how you'd assert on a message eventually arriving without Thread.sleep — this is the expected answer.

on this pageshow

explore

questions

5

Kotest's assertion library ships `eventually`, `until` and `continually` for testing asynchronous behaviour. What does each one assert, and how do their pass and fail conditions differ?

level: middleimportance: must knowfreq 45%

answer

  1. eventually = first success wins
  2. continually = first failure loses
  3. until = boolean predicate flavour
  4. eventually returns the block's value
  5. continually always burns its full window

basics

~20 s

eventually re-runs a block until it stops throwing, within a deadline — the first success passes. until does the same for a boolean predicate becoming true. continually requires the block to keep passing for the whole duration — the first failure fails the test.

solid answer

~50 s

All three come from Kotest's nondeterministic assertions and are suspend functions, so they work directly in a Kotest test body. - **`eventually(duration) { ... }`** polls a block that contains assertions. Failures (thrown `AssertionError`s or exceptions) are swallowed and retried until either the block completes normally — which passes immediately and returns the block's value — or the deadline expires, at which point the test fails with the accumulated attempt information. - **`until(duration) { boolean }`** is the same polling loop over a boolean predicate: it succeeds the first time the predicate returns `true`. - **`continually(duration) { ... }`** inverts the condition: the block must succeed on *every* invocation for the full duration. The first failure aborts and fails the test; surviving the whole window passes. So `eventually`/`until` prove "this becomes true", `continually` proves "this never stops being true" — for example that a value does *not* change after an event.

code

kotlin · 20 lines
kotlin
class OutboxTest : FunSpec({
    test("message is projected into the read model") {
        publisher.publish(OrderPlaced(orderId))

        val row = eventually(5.seconds) {
            readModel.findOrder(orderId).shouldNotBeNull()
        }
        row.status shouldBe "PLACED"
    }

    test("a duplicate message is not projected twice") {
        publisher.publish(OrderPlaced(orderId))
        eventually(5.seconds) { readModel.findOrder(orderId).shouldNotBeNull() }

        publisher.publish(OrderPlaced(orderId))
        continually(2.seconds) {
            readModel.countOrders(orderId) shouldBe 1
        }
    }
})

go deeper

for a junior

Know the one-line contract of each: eventually retries until success, continually requires continuous success, until polls a boolean. Be able to write a simple eventually(5.seconds) { ... }.

for a middle

Explain first-success-wins vs first-failure-loses, that eventually returns the block's value, and why the trigger goes outside the block. Name the negative-assertion case that only continually covers.

for a senior

Discuss diagnostics (why matcher-based eventually beats boolean until), idempotency of the polled block, and the cost model — continually always spends its full window while eventually usually finishes early.

for a principal

Frame it as a suite-wide policy: polling is a fallback for genuine asynchrony, deterministic signals are preferred, and deadline budgets must be set so CI degrades visibly rather than silently doubling in duration.

## The problem these solve A lot of real behaviour is not observable the instant you trigger it: a message is consumed by a background listener, a cache invalidation propagates, a file is flushed, an HTTP server finishes starting. A plain assertion right after the trigger is a race — it passes on a fast machine and fails on a loaded CI box. Sprinkling `Thread.sleep(2000)` "fixes" it by making every run slow *and* still flaky, because the sleep is a guess. Kotest's answer is a small family of polling helpers in its assertions library. They are all `suspend` functions, which matters: Kotest test bodies are themselves suspending, so you call them directly, and the waiting between attempts is a coroutine `delay` rather than a blocked thread. ## `eventually` — "this becomes true before the deadline" ```kotlin eventually(5.seconds) { repo.findById(id).shouldNotBeNull() } ``` Semantics: run the block. If it returns normally, `eventually` returns that value immediately and the assertion is satisfied. If it throws — an `AssertionError` from a matcher, or any other exception, since by default all throwables are treated as retryable — the error is captured, the coroutine delays for the configured interval, and the block runs again. When the deadline passes without a success, `eventually` throws, reporting how many attempts ran, how long it took, and the errors it saw. Two consequences people miss. First, **the block must be idempotent and re-runnable**: it will execute many times, so putting the *trigger* (posting the message, calling the endpoint) inside `eventually` means you trigger it repeatedly. Put the trigger outside and only the observation inside. Second, **the first success wins** — `eventually` proves nothing about what happens afterwards. ## `until` — the boolean flavour ```kotlin until(5.seconds) { queue.isEmpty() } ``` `until` polls a predicate and succeeds when it first returns `true`. It is essentially `eventually` wrapped around a truth check, and the practical difference is diagnostics: a failing `until` can only say "the predicate never became true", whereas a failing `eventually` carries the matcher's message ("expected 3 but was 1"), which is far more useful in a CI log. Prefer `eventually` with real matchers unless the condition genuinely is a bare boolean. ## `continually` — "this stays true for the whole window" ```kotlin continually(2.seconds) { account.balance shouldBe 100 } ``` `continually` repeatedly invokes the block for the given duration and requires every invocation to pass. The first failure ends it and fails the test; if the window elapses with no failure, it passes. This is the assertion for *negative* propositions that `eventually` cannot express. "After I publish this event, the read model must **not** change" is unprovable with `eventually` — a block asserting no-change passes on its very first attempt, before anything could have happened. `continually` holds the assertion open long enough to catch a late, wrong write. Classic uses: a duplicate message is not reprocessed, a circuit breaker stays open for its cooldown, a debounced action does not fire early. The cost is honest: `continually(2.seconds)` always spends the full two seconds. Unlike `eventually`, it cannot finish early, so keep its windows short and its uses few. ## Choosing between them - Something must *appear* or *converge* → `eventually`. - Something must *not appear* or must *stay stable* → `continually`. - A bare boolean flag flips and you do not care about the failure message → `until`. ## Failure output and tuning When `eventually` gives up, its message includes the elapsed time, the number of attempts and the failures observed, so a red build tells you whether the condition never got close or was one poll away. That is what makes it debuggable, unlike a bare sleep. Both `eventually` and `continually` accept a configuration object as an alternative to a bare duration, letting you set the polling interval, an initial delay before the first attempt, a retry cap, and which exception types are treated as retryable. Defaults poll aggressively, which is usually right for in-process assertions and too aggressive for network calls. ## Pitfalls - Wrapping an entire scenario in `eventually` — it re-runs side effects and hides which step is slow. Poll the narrowest observable. - Using `eventually` where the code under test can be made deterministic (an injected clock, a completion signal, an awaited future). Polling is a fallback for genuine asynchrony, not a substitute for design. - Long deadlines as flakiness insurance: a 60-second `eventually` turns one broken assumption into a minute of dead CI time per run. - Assuming a passing `eventually` means the state is stable. It means it was true once.

  • Why can't you express "nothing happens" with `eventually`?
    `eventually` stops at the first successful invocation. A block asserting that a value is unchanged succeeds on attempt one, microseconds after the trigger, long before a late or erroneous write could occur — so it proves nothing. `continually` keeps re-checking for the whole window, so a wrong write arriving 800ms later still fails the test.
  • What should and should not go inside the `eventually` block?
    Only the observation: the read, the query, the matcher. The action that starts the asynchronous work belongs before the call, because the block is invoked repeatedly — a trigger inside it would publish the message or issue the command once per poll, corrupting the very state you are asserting on. The block should be idempotent and side-effect free.
  • Why prefer `eventually` with matchers over `until` with a boolean?
    Diagnostics. A failed `until` can only report that the predicate never became true. A failed `eventually` propagates the matcher's own message — the expected and actual values — plus attempt count and elapsed time, which is usually the difference between debugging from the CI log and having to reproduce locally.

eventually is watching for the kettle to boil — you stop the moment it does. continually is standing guard for a fixed shift — you only fail if something goes wrong before your shift ends.

saying these in an interview costs you the question

  • Believing `continually` stops early on success — it always runs the full duration and only stops early on failure.
  • Putting the triggering action inside the `eventually` block, so it fires once per poll.
  • Treating `eventually` as proof that a condition is stable rather than that it held once.
  • Reaching for `eventually` when the asynchrony could be made deterministic with an injected clock or an awaited completion signal.
  • Claiming these are blocking sleeps — they are suspend functions that `delay` between attempts.

context

open as a page

When a Kotest `eventually` block finally succeeds, what does the call give back, and what does the failure output contain when it never succeeds? How does that shape the way you write the polled block?

level: middleimportance: should knowfreq 26%

basics

~20 s

eventually returns the value produced by the successful invocation, so you can bind the resolved object and assert further on it outside the loop. On timeout it fails with the elapsed time, the attempt count and the failures it saw — which is why matcher-based blocks debug far better than boolean ones.

open as a page

Kotest's `eventually` can take a configuration object built with `eventuallyConfig { }` instead of a bare duration. Which knobs does it expose, and what happens when the polled block throws an exception that is not in the configured expected-exception set?

level: seniorimportance: should knowfreq 33%

basics

~20 s

You can set the total duration, an initial delay, the polling interval (fixed or backing off), a retry cap, a per-iteration listener and a short-circuit predicate. If expectedExceptions is set, only those are swallowed and retried — any other throwable fails the test immediately instead of being polled through.

open as a page

Kotest's assertion library provides both `retry` and `eventually`. They both re-run a block, so what is the actual difference in intent and semantics, and when would you choose `retry`?

level: seniorimportance: should knowfreq 28%

basics

~20 s

eventually polls an observation until an assertion holds before a deadline — the block must be side-effect free. retry re-executes a whole flaky operation up to a maximum attempt count, with a delay and an optional backoff multiplier, and only for a named exception class. Retry acts; eventually observes.

open as a page

A large Kotlin test suite has accumulated dozens of Kotest `eventually` and `continually` calls, and CI time and flakiness are both climbing. How would you decide where polled assertions belong at all, and what policy would you set for their budgets?

level: principalimportance: nice to knowfreq 22%

basics

~20 s

Treat polling as a fallback for genuine cross-process asynchrony only. Everywhere the code can expose a deterministic signal — an injected clock, an awaited completion, a synchronous test wiring — remove the wait. For what remains, standardise a small set of shared budgets, keep polled scopes narrow, and track total waiting as a metric.

open as a page