skip to content

When and how do you use expectNoEvents() in Turbine, and what's a common pitfall with it?

level: middleimportance: should knowfreq 40%

answer

  1. Asserts queue is empty right now (no item/complete/error)
  2. Non-suspending — does not wait
  3. Proves a negative: filtered/debounced/no-op
  4. Advance virtual time (runCurrent/advanceTimeBy) first
  5. A pending terminal makes it fail

basics

~10 s

expectNoEvents() checks that, right now, the flow has not produced any new value, completion, or error. You use it to prove something did NOT happen yet, like after a filtered input.

solid answer

~40 s

expectNoEvents() is a non-suspending assertion that fails if Turbine's event queue currently holds any unconsumed event — item, completion, or error. It's the tool for proving a negative: e.g. a debounced or filtered flow should NOT emit for a given input, or a UI state flow should stay idle until an action. The pitfall is timing: expectNoEvents() checks the queue at the instant it's called and does not wait. Under runTest, you must ensure the producer coroutine has actually had a chance to run (advance virtual time, e.g. with runCurrent() or advanceTimeBy()) before asserting no events, otherwise the assertion passes only because the emission hasn't been scheduled yet — a false sense of safety. It complements awaitItem rather than replacing it.

code

kotlin · 9 lines
kotlin
@Test
fun filtersOddNumbers() = runTest {
    flowOf(2, 3, 4).filter { it % 2 == 0 }.test {
        assertEquals(2, awaitItem())
        assertEquals(4, awaitItem())
        awaitComplete()
        // no expectNoEvents needed here; queue must be empty to complete cleanly
    }
}

go deeper

for a junior

Knows it checks that nothing was emitted.

for a middle

Knows it's non-suspending and used to assert filtering/debounce produced no output.

for a senior

Explains the virtual-time race and uses runCurrent/advanceTimeBy to make the assertion sound.

for a principal

Articulates testing-for-negatives risk in async tests and patterns to avoid flaky 'no-event' assertions across a codebase.

## Purpose: asserting a negative `expectNoEvents()` asserts that **at the moment it is called**, no item, completion, or error is waiting in Turbine's queue. It is the canonical way to test that the flow **did not emit** — for filtering, debouncing, deduplication (`distinctUntilChanged`), or gating logic. ```kotlin flow.test { assertEquals(initial, awaitItem()) triggerSomethingThatShouldBeIgnored() expectNoEvents() // proves the trigger produced nothing } ``` ## It does NOT wait Unlike `awaitItem()`, `expectNoEvents()` is **instantaneous** — it inspects the current queue and returns or fails immediately. This is the source of the classic pitfall: if the producer hasn't been scheduled yet, the queue is naturally empty and the assertion passes **for the wrong reason**. ## The virtual-time pitfall and fix Under `kotlinx.coroutines.test.runTest`, coroutines run on a `TestCoroutineScheduler` with **virtual time**. A `delay()` or a not-yet-dispatched emission won't appear until you advance the scheduler. So to make `expectNoEvents()` meaningful you usually: - call **`runCurrent()`** to run all tasks scheduled at the current virtual time, or - call **`advanceTimeBy(...)`** to push past a debounce window, before asserting no events. Example with `debounce`: ```kotlin @Test fun debounceSwallowsRapidInput() = runTest { val input = MutableSharedFlow<Int>() input.debounce(100).test { input.emit(1) advanceTimeBy(50) runCurrent() expectNoEvents() // 50ms < 100ms window: nothing yet advanceTimeBy(60) runCurrent() assertEquals(1, awaitItem()) // window elapsed -> emits cancelAndIgnoreRemainingEvents() } } ``` ## When to prefer it - After an input that **should** be filtered out. - To assert quiescence before triggering the next step. - Paired with time advancement to prove ordering across delays. ## What it is NOT for - It is not a substitute for `awaitComplete()`/`awaitError()`; a completed flow has a pending terminal event, so `expectNoEvents()` would fail. - It does not consume events; it only checks emptiness.

  • Why might expectNoEvents() pass even though the flow would emit?
    Because it doesn't wait — if the producing coroutine hasn't run yet under virtual time, the queue is empty. Advance time (runCurrent/advanceTimeBy) before asserting.
  • Can you call expectNoEvents() after awaitComplete()?
    After completion there are no further events, so it can hold, but it's redundant; once you've consumed the terminal, the test block ends anyway.

It's a snapshot 'is the inbox empty?' check, not a 'wait for mail' — so make sure the mail truck has already had its chance to deliver.

saying these in an interview costs you the question

  • Believing expectNoEvents() waits/suspends for a while
  • Using it without advancing virtual time, then trusting the pass
  • Thinking it consumes the next event
  • Calling it on a completed flow expecting it to pass with a pending Complete

context