# cashapp/turbine: testing kotlinx.coroutines Flow without flaky timeouts

> Turbine wraps a Flow in a Channel-backed test harness so you can await items, completion and errors in order. It suits Kotlin teams already on kotlinx.coroutines, and it has one documented dependency risk worth knowing before adoption.

**cashapp/turbine** — A testing library for kotlinx.coroutines Flow

- Repository: https://github.com/cashapp/turbine
- Website: https://cashapp.github.io/turbine/docs/1.x/
- Stars: 2,861 · Forks: 136
- Language: Kotlin
- License: Apache-2.0
- Published: 2026-09-28 · Updated: 2026-09-28 · Language: en
- Canonical page: https://hysenlabs.com/projects/cashapp-turbine

## The problem Turbine solves for Kotlin Flow tests

Testing a Flow by hand means collecting it into a list inside a coroutine, advancing virtual time, and hoping the emissions have arrived before your assertions run. That pattern is where flaky Kotlin tests come from. Turbine replaces it with a small receiver object that suspends until the next event is available, so assertions read in the same order the flow emits.

The audience is narrow and specific: Kotlin developers who already test with kotlinx.coroutines and want to assert on a Flow's items, its completion, or the exception that terminates it. The README's opening example shows the shape of that API. The library is published as app.cash.turbine:turbine on Maven Central, licensed Apache-2.0, and the latest release listed is 1.2.1 from 2025-06-11. This is a test-scope dependency, not something that ships in your application.

## How Turbine turns a Flow into awaitable events

The README describes a Turbine as a thin wrapper over a Channel with an API designed for testing. That sentence explains most of the behaviour. A Channel carries discrete events, and Turbine exposes them through three suspending calls: awaitItem() for an emitted value, awaitComplete() for normal termination, and awaitError() for termination with a Throwable. If an await call finds nothing, Turbine times out and fails the test rather than blocking forever, which is what keeps a broken flow from turning into a hung build.

The test extension is the entry point for a single flow. According to the README, it launches a new coroutine, calls collect on your flow, and feeds the results into a Turbine. Your validation block then runs with a read-only ReceiveTurbine as receiver. When the block finishes, test cancels the coroutine and calls ensureAllEventsConsumed().

That last call is the design decision worth noticing. Termination is modelled as an event, not as control flow. A RuntimeException thrown inside your flow does not propagate out of the test; it becomes an error event you must consume. Left unconsumed, it surfaces as a TurbineAssertionError whose cause is the original exception, so the stack trace is still available. The trade-off is that a test which simply ignores the end of a flow will fail loudly, which is deliberate.

## Installing Turbine and writing a first Flow test

Turbine is distributed through Maven Central. The README gives this dependency block, which belongs in your build file with the rest of your test dependencies.

```kotlin
repositories {
  mavenCentral()
}
dependencies {
  testImplementation("app.cash.turbine:turbine:1.2.1")
}
```

After a Gradle sync, the test source set can resolve app.cash.turbine. The README also documents a snapshot channel at https://central.sonatype.com/repository/maven-snapshots/ for version 1.3.0-SNAPSHOT, which is useful only if you want to track development builds; for normal work, stay on the released 1.2.1.

The first real test is the README's own example. It asserts each item in order and then asserts completion, which is the pattern most Flow tests reduce to.

```kotlin
flowOf("one", "two").test {
  assertEquals("one", awaitItem())
  assertEquals("two", awaitItem())
  awaitComplete()
}
```

If you run this and the flow emits something else, the failure names the unexpected event. If you omit awaitComplete(), the test fails with an unconsumed-events assertion listing Item and Complete, which is the behaviour the README documents explicitly. For a flow that fails, the equivalent is to assert on awaitError() and read its message, for example assertEquals("broken!", awaitError().message).

## Multiple flows, testIn, and the hang you can cause yourself

A single flow is the easy case. When a presenter consumes one flow and drives another, the README points to testIn, which returns a ReceiveTurbine you assign to a val instead of running a validation block. The documented pattern nests turbineScope inside runTest and passes backgroundScope to each testIn call, so the test framework owns the coroutine lifecycle.

```kotlin
runTest {
  turbineScope {
    val turbine1 = flowOf(1).testIn(backgroundScope)
    val turbine2 = flowOf(2).testIn(backgroundScope)
    assertEquals(1, turbine1.awaitItem())
    assertEquals(2, turbine2.awaitItem())
    turbine1.awaitComplete()
    turbine2.awaitComplete()
  }
}
```

The README is blunt about the failure mode: testIn cannot automatically clean up its coroutine, so if you do not use backgroundScope you must call cancel(), awaitComplete(), or awaitError() before the end of your scope, or your test will hang. That is a real constraint, not a stylistic preference. It is the one place where Turbine's convenience depends on you understanding structured concurrency.

Ordering also matters here. Because each turbine is an independent channel, the two awaitItem calls above do not have to interleave in the order the flows emit; each turbine queues its own events. That property is why the multi-flow pattern works at all, and why tests written this way tend to survive refactors of the production code's scheduling.

## Ignoring events on purpose and reading the most recent one

Not every test needs to assert every emission. The README offers cancelAndIgnoreRemainingEvents() for the case where you have validated the interesting prefix and want to stop. It also offers expectMostRecentItem() for flows that emit faster than you want to step through, returning the latest buffered item and discarding earlier ones. The documented example delays 250ms against a flow emitting every 100ms and asserts the second item, then cancels the rest.

Both are escape hatches, and both weaken the guarantee that you checked everything. A test that ends in cancelAndIgnoreRemainingEvents() will not catch a regression in the tail of the flow. That is a reasonable trade when the tail is noise, and a bad one when the tail is the point. Turbine gives you the tool; it does not decide for you.

There is a second use for the same object that the README highlights: standalone Turbine instances, created directly with Turbine<Screen>() and fed via add(), so a fake can record calls without a Flow in the picture. The README claims that used this way, you might never need runCurrent() again. That is a plausible consequence of replacing virtual-time advancement with suspending awaits, though it is the README's framing rather than a measured result.

## The UnconfinedTestDispatcher dependency is the real caveat

Turbine's own API is described as stable, but the README states plainly that the library is currently forced to depend on an unstable API from the kotlinx.coroutines test artifact: UnconfinedTestDispatcher. Without that usage, the README says, Turbine with runTest would break. The consequence is that future coroutine library updates could alter Turbine's behaviour, and the maintainers track the issue as #132.

Read that as a compatibility boundary rather than a defect. If your project pins kotlinx.coroutines and upgrades it deliberately, you are exposed to the same risk you already carry for any test library built on that artifact. If you upgrade coroutines aggressively across a large suite, a behaviour change in dispatcher semantics could show up as reordered emissions or timeouts in Turbine tests, and the failure would look like your test rather than your dependency.

The other limitation is scope. Turbine tests Flow. It is not a general coroutine testing framework, it does not replace runTest, and it has nothing to say about non-coroutine code. If your tests are not already coroutine-based, adding Turbine buys you little.

## Turbine versus collecting into a list yourself

The obvious alternative is to collect the flow into a list inside runTest and assert on the result, using the coroutine test utilities you already have. That approach needs no extra dependency and works fine for finite flows that complete quickly.

The difference is in what happens when the flow does not behave. A list-based test blocks until the flow completes or the test times out at the framework level, and the failure tells you the test timed out, not which emission was missing. Turbine's awaits fail individually with a timeout, and its unconsumed-event check names the specific events you skipped. That is the whole argument for the dependency: better failure messages and per-step assertions, at the cost of a test-scope library and its dispatcher coupling.

If your flows are short, synchronous, and rarely change, the list approach is genuinely simpler and you should not feel obliged to replace it. Turbine earns its place in suites where flows are long-lived, where ordering is part of the contract, or where a hang costs more than a failed assertion.

## Maintenance, releases and licence

The repository is not archived, and its last push was on 2026-09-23, which is recent. The release history is slower than the commit cadence: 1.1.0 in March 2024, 1.2.0 in October 2024, and 1.2.1 in June 2025. If you depend on released artifacts rather than snapshots, expect fixes to arrive in versioned batches rather than continuously.

Upgrade cost is low for a test-only dependency. A version bump in testImplementation is the whole change in most cases, and the README's note about snapshot builds means you can preview development versions from the Central Portal Snapshots repository if you need a fix before a release. The risk to watch is not Turbine's own API; it is the kotlinx.coroutines version you resolve alongside it.

The project is Apache-2.0, which permits commercial and closed-source use with the usual attribution and notice obligations. This is a summary of the licence identifier, not legal advice; check the terms against your own distribution model.

## Conclusion

Adopt Turbine if you write Kotlin tests around Flow and already use kotlinx.coroutines test; the awaitItem, awaitComplete and awaitError API removes hand-rolled collection and timeout code, and the unconsumed-event assertion catches partial validation. Do not adopt it if your tests are not coroutine-based, or if you need a promise that behaviour cannot shift with future kotlinx.coroutines releases, because the README states the library depends on the unstable UnconfinedTestDispatcher API. Before wiring it into a large suite, verify two things on your own toolchain: that the version you resolve matches the one you declared in testImplementation, and that your test dispatcher setup produces the ordering your assertions assume.

## FAQ

### How do I install Turbine in a Gradle project?

Add mavenCentral() to your repositories and app.cash.turbine:turbine:1.2.1 as a testImplementation dependency, as shown in the README's download section. Snapshot builds of 1.3.0-SNAPSHOT are also published to the Central Portal Snapshots repository.

### What is Turbine used for in Kotlin tests?

It is a testing library for kotlinx.coroutines Flow, exposing awaitItem(), awaitComplete() and awaitError() so a test can assert on emissions and termination in order. It also fails the test if events are left unconsumed.

### Why does my Turbine test hang instead of failing?

The README states that testIn cannot automatically clean up its coroutine, so you must call cancel(), awaitComplete() or awaitError() before the end of your scope. Using runTest's backgroundScope handles this automatically.

### Does Turbine work with runTest and multiple flows?

Yes. The README documents testIn for multiple flows, used inside turbineScope within runTest, with each ReceiveTurbine assigned to its own val and each flow given backgroundScope.

## Sources

- [cashapp/turbine on GitHub](https://github.com/cashapp/turbine)
- [License: Apache-2.0](https://github.com/cashapp/turbine/blob/trunk/LICENSE)
- [Project website](https://cashapp.github.io/turbine/docs/1.x/)
- [README](https://github.com/cashapp/turbine/blob/trunk/README.md)
- [Releases](https://github.com/cashapp/turbine/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/cashapp-turbine
