Turbine - 用于测试 Kotlin Flow的开源库

431 阅读3分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

Turbine

Turbine是一个小的测试包, 用来测试 kotlinx.coroutines扩展包中的 Flow.

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

涡轮是一个旋转机械设备, 从流体中解析能量并转化成有用功.

– Wikipedia

从Maven中下载

repositories {
  mavenCentral()
}
dependencies {
  testImplementation 'app.cash.turbine:turbine:0.10.0'
}

开发版本的快照在Sonatype的快照仓库中可用.

用法

该包的入口点是Flow<T>test函数, 它接收了验证闭包作为参数. 就像collecttest是一个挂起函数, 直到流完成或者取消了才会返回结果.

someFlow.test {
  // Validation code here!
}

消费事件

test模块里面, 你必须消费掉全部从流中接收到的事件. 不能消费全部事件将使测试失败.

flowOf("one", "two").test {
  assertEquals("one", awaitItem())
}
Exception in thread "main" AssertionError:
  Unconsumed events found:
   - Item(two)
   - Complete

像上面的异常所揭示的一样, 消费掉"two"数据项是不足够的. 已经完成事件必须也被消费掉T.

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

然而, 已接收事件能够被显式地忽略掉.

flowOf("one", "two").test {
  assertEquals("one", awaitItem())
  cancelAndIgnoreRemainingEvents()
}

除此之外, 我们也能够接收最近发出的数据项并且忽略掉前面的数据项.

flowOf("one", "two", "three")
  .map {
    delay(100)
    it
  }
  .test {
    // 0 - 100ms -> no emission yet
    // 100ms - 200ms -> "one" is emitted
    // 200ms - 300ms -> "two" is emitted
    // 300ms - 400ms -> "three" is emitted
    delay(250)
    assertEquals("two", expectMostRecentItem())
    cancelAndIgnoreRemainingEvents()
  }

消费错误

不像collect, 引发异常的流依然会作为必须消费的事件而暴露.

flow { throw RuntimeException("broken!") }.test {
  assertEquals("broken!", awaitError().message)
}

消费错误失败将导致如上所示的未消费事件异常, 但是有了异常作为cause添加了, 完整的栈迹是可以访问的.

flow { throw RuntimeException("broken!") }.test { }
java.lang.AssertionError: Unconsumed events found:
 - Error(RuntimeException)
    at app.cash.turbine.ChannelBasedFlowTurbine.ensureAllEventsConsumed(FlowTurbine.kt:240)
    ... 53 more
Caused by: java.lang.RuntimeException: broken!
    at example.MainKt$main$1.invokeSuspend(Main.kt:7)
    ... 32 more

异步流

awaitItem()awaitComplete()awaitError()的调用是挂起的, 将会等待源于异步流的事件.

channelFlow {
  withContext(IO) {
    Thread.sleep(100)
    send("item")
  }
}.test {
  assertEquals("item", awaitItem())
  awaitComplete()
}

异步流可以在任何时候取消, 只要你已经消费了全部已发出事件. 允许完成test的lambda将隐式地取消该流.

channelFlow {
  withContext(IO) {
    repeat(10) {
      Thread.sleep(200)
      send("item $it")
    }
  }
}.test {
  assertEquals("item 0", awaitItem())
  assertEquals("item 1", awaitItem())
  assertEquals("item 2", awaitItem())
}

Flow也能够在任何时候显式地取消.

channelFlow {
  withContext(IO) {
    repeat(10) {
      Thread.sleep(200)
      send("item $it")
    }
  }
}.test {
  Thread.sleep(700)
  cancel()

  assertEquals("item 0", awaitItem())
  assertEquals("item 1", awaitItem())
  assertEquals("item 2", awaitItem())
}

热流

没有活跃消费者的热流的发射数据将会被丢弃. 要在数据发射之前在流上调用test否则数据项会丢失.

val mutableSharedFlow = MutableSharedFlow<Int>(replay = 0)
mutableSharedFlow.emit(1)
mutableSharedFlow.test {
  assertEquals(awaitItem(), 1)
}
kotlinx.coroutines.test.UncompletedCoroutinesError: After waiting for 60000 ms, the test coroutine is not completing, there were active child jobs: [ScopeCoroutine{Completing}@478db956]
	at app//kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTestCoroutine$3$3.invokeSuspend(TestBuilders.kt:304)
	at ???(Coroutine boundary.?(?)
	at kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt.runTestCoroutine(TestBuilders.kt:288)
	at kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTest$1$1.invokeSuspend(TestBuilders.kt:167)
	at kotlinx.coroutines.test.TestBuildersJvmKt$createTestResult$1.invokeSuspend(TestBuildersJvm.kt:13)

在Turbine上使用热流的常规用法看起来像下面这样.

val mutableSharedFlow = MutableSharedFlow<Int>(replay = 0)
mutableSharedFlow.test {
  mutableSharedFlow.emit(1)
  assertEquals(awaitItem(), 1)
}

Kotlin当前提供的热流类型是:

  • MutableStateFlow
  • StateFlow
  • MutableSharedFlow
  • SharedFlow
  • 使用Channel.consumeAsFlow函数将Channel转化成Flow

多个turbine

通过使用testIn函数, 多个流将能够一同测试, 这个函数返回了turbine测试对象, 这个对象会在test函数中作为lambda接收器.

runTest {
  val turbine1 = flowOf(1).testIn(this)
  val turbine2 = flowOf(2).testIn(this)
  assertEquals(1, turbine1.awaitItem())
  assertEquals(2, turbine2.awaitItem())
  turbine1.awaitComplete()
  turbine2.awaitComplete()
}

未消费事件将会在作用域结束的时候抛出异常.

runTest {
  val turbine1 = flowOf(1).testIn(this)
  val turbine2 = flowOf(2).testIn(this)
  assertEquals(1, turbine1.awaitItem())
  assertEquals(2, turbine2.awaitItem())
  turbine1.awaitComplete()
  // turbine2.awaitComplete()   <-- NEWLY COMMENTED OUT
}
kotlinx.coroutines.CompletionHandlerException: Exception in completion handler InvokeOnCompletion@6d167f58[job@3403e2ac] for TestScope[test started]
	at app//kotlinx.coroutines.JobSupport.completeStateFinalization(JobSupport.kt:320)
	at app//kotlinx.coroutines.JobSupport.tryFinalizeSimpleState(JobSupport.kt:295)
	... 70 more
Caused by: app.cash.turbine.AssertionError: Unconsumed events found:
 - Complete
	at app//app.cash.turbine.ChannelBasedFlowTurbine.ensureAllEventsConsumed(FlowTurbine.kt:333)
	at app//app.cash.turbine.FlowTurbineKt$testIn$1.invoke(FlowTurbine.kt:115)
	at app//app.cash.turbine.FlowTurbineKt$testIn$1.invoke(FlowTurbine.kt:112)
	at app//kotlinx.coroutines.InvokeOnCompletion.invoke(JobSupport.kt:1391)
	at app//kotlinx.coroutines.JobSupport.completeStateFinalization(JobSupport.kt:318)
	... 72 more

不像test的lambda, 流并没有自动取消. 长时间运行的异步流或者无限流必须显式地取消.

runTest {
  val state = MutableStateFlow(1)
  val turbine = state.testIn(this)
  assertEquals(1, turbine.awaitItem())
  state.emit(2)
  assertEquals(2, turbine.awaitItem())
  turbine.cancel()
}