Skip to content

What it is

Turbine is a small testing library for Kotlin Flows, providing a clear API for asserting on emissions, completion and errors.

Call .test { } on a Flow and consume emissions with awaitItem(). Turbine fails the test if items are left unconsumed or expected ones never arrive.

Installation

testImplementation("app.cash.turbine:turbine:1.1.0")

Getting started

The smallest useful thing you can do with it, and what each part means.

Asserting on emissions
@Test
fun `emits loading then content`() = runTest {
    viewModel.state.test {
        assertEquals(UiState.Loading, awaitItem())

        viewModel.load()

        val ready = awaitItem() as UiState.Ready
        assertEquals(3, ready.books.size)

        // Fails the test if anything else was emitted.
        cancelAndIgnoreRemainingEvents()
    }
}

@Test
fun `propagates errors`() = runTest {
    failingFlow.test {
        assertEquals(1, awaitItem())
        assertTrue(awaitError() is IOException)
    }
}
awaitItem suspends until the next emission and times out otherwise, so a Flow that never emits fails with a clear message rather than hanging the suite.

Advanced usage

Where the library earns its place over a simpler alternative.

Virtual time and hot flows
@Test
fun `debounces rapid input`() = runTest {
    // runTest's scheduler skips delays instantly — no real waiting.
    searchViewModel.results.test {
        skipItems(1)                       // initial empty state

        searchViewModel.onQueryChanged("du")
        searchViewModel.onQueryChanged("dun")
        searchViewModel.onQueryChanged("dune")

        advanceTimeBy(301)                 // past the debounce window

        val results = awaitItem()
        assertEquals(1, results.size)      // only the last query ran
        cancelAndIgnoreRemainingEvents()
    }
}
runTest's virtual clock means a 300 ms debounce test finishes instantly. Combined with Turbine, timing-dependent Flow logic becomes deterministically testable.

Errors and fixes

The failures you are most likely to hit, and what actually resolves them.

Unconsumed events found
The flow emitted more than the test consumed. Assert on them, or end with cancelAndIgnoreRemainingEvents.
No value produced in 3s
The flow never emitted — often a dispatcher not controlled by the test. Inject a TestDispatcher.

Best practices

  • Always finish with cancelAndIgnoreRemainingEvents or awaitComplete; unconsumed events fail the test by design.
  • Use runTest and advanceTimeBy for time-based operators rather than real delays.
  • Test StateFlow expecting the current value first — it replays immediately on collection.
  • Keep each test to one behaviour; long emission sequences are hard to diagnose.

Background

Why it exists, and what it was reacting to.

From Cash App, Turbine solves a real problem: testing a Flow with collect and a mutable list is racy and produces confusing failures. Turbine makes each expected emission an explicit assertion.