When and how do you use expectNoEvents() in Turbine, and what's a common pitfall with it?
answer
- Asserts queue is empty right now (no item/complete/error)
- Non-suspending — does not wait
- Proves a negative: filtered/debounced/no-op
- Advance virtual time (runCurrent/advanceTimeBy) first
- A pending terminal makes it fail
basics
~10 sexpectNoEvents() 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 sexpectNoEvents() 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@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
Knows it checks that nothing was emitted.
Knows it's non-suspending and used to assert filtering/debounce produced no output.
Explains the virtual-time race and uses runCurrent/advanceTimeBy to make the assertion sound.
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