本文已参与「新人创作礼」活动,一起开启掘金创作之路。
Turbine
Turbine是一个小的测试包, 用来测试 kotlinx.coroutines扩展包中的 Flow
.
flowOf("one", "two").test {
assertEquals("one", awaitItem())
assertEquals("two", awaitItem())
awaitComplete()
}
涡轮是一个旋转机械设备, 从流体中解析能量并转化成有用功.
从Maven中下载
repositories {
mavenCentral()
}
dependencies {
testImplementation 'app.cash.turbine:turbine:0.10.0'
}
开发版本的快照在Sonatype的快照仓库中可用.
用法
该包的入口点是Flow<T>
的test
函数, 它接收了验证闭包作为参数. 就像collect
, test
是一个挂起函数, 直到流完成或者取消了才会返回结果.
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()
}