渦輪機是Kotlinx Flow小型測試庫。
flowOf( " one " , " two " ).test {
assertEquals( " one " , awaitItem())
assertEquals( " two " , awaitItem())
awaitComplete()
}渦輪機是一種旋轉機械裝置,可從流體流中提取能量並將其轉換為有用的工作。
- 維基百科
repositories {
mavenCentral()
}
dependencies {
testImplementation( " app.cash.turbine:turbine:1.2.0 " )
}repositories {
maven {
url = uri( " https://oss.sonatype.org/content/repositories/snapshots/ " )
}
}
dependencies {
testImplementation( " app.cash.turbine:turbine:1.3.0-SNAPSHOT " )
}雖然渦輪機自己的API穩定,但目前我們被迫依靠Kotlinx的不穩定API。Coroutinestest Artifact: UnconfinedTestDispatcher 。如果沒有這種渦輪機對runTest的使用,將會破裂。未來的Coroutine庫更新可能會更改此庫的行為。我們將盡一切努力確保行為穩定性,直到該API依賴性穩定為止(跟踪問題#132)。
Turbine是一條薄的包裝器,其Channel上的API設計用於測試。
您可以致電awaitItem()暫停並等待將物品發送到Turbine :
assertEquals( " one " , turbine.awaitItem()) ... awaitComplete()暫停,直到Turbine完成沒有例外:
turbine.awaitComplete() ...或awaitError()暫停,直到Turbine以Throwable為止。
assertEquals( " broken! " , turbine.awaitError().message)如果await* ,什麼也沒發生, Turbine將超時和失敗而不是懸掛。
使用Turbine完成後,可以通過調用cancel()來清理以終止任何備用旋風。最後,您可以斷言所有事件都是通過致電ensureAllEventsConsumed()來消耗的。
創建和運行Turbine最簡單方法是從Flow中產生一種。要測試單個Flow ,請調用test擴展名:
someFlow.test {
// Validation code here!
} test啟動了一個新的Coroutine,稱為someFlow.collect ,並將結果饋送到Turbine中。然後,它調用驗證塊,以接收者的身份傳遞僅讀取的ReceiveTurbine接口:
flowOf( " one " ).test {
assertEquals( " one " , awaitItem())
awaitComplete()
}驗證塊完成後, test取消coroutine並調用ensureAllEventsConsumed() 。
要測試多個流量,請通過調用testIn來分配每個Turbine val
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()
}
}像test一樣, testIn會產生ReceiveTurbine 。呼叫Coroutine完成後,將調用ensureAllEventsConsumed() 。
testIn無法自動清理其Coroutine,因此可以確保運行流終止。使用runTest的backgroundScope ,它將自動處理。否則,請確保在範圍結束之前調用以下方法之一:
cancel()awaitComplete()awaitError()否則,您的測試將懸掛。
在基於流Turbine的驗證塊結束之前,未能消耗所有事件,這將使您的測試失敗:
flowOf( " one " , " two " ).test {
assertEquals( " one " , awaitItem())
} Exception in thread "main" AssertionError:
Unconsumed events found:
- Item(two)
- Complete
testIn也是如此,但在通話的結尾處:
runTest {
turbineScope {
val turbine = flowOf( " one " , " two " ).testIn(backgroundScope)
turbine.assertEquals( " one " , awaitItem())
}
} Exception in thread "main" AssertionError:
Unconsumed events found:
- Item(two)
- Complete
但是,可以明確忽略收到的事件。
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()
}流程終止事件(例外和完成)被視為必須消耗的事件才能驗證。因此,例如,在您的flow中拋出一個RuntimeException會不會在測試中引發例外。它將產生渦輪錯誤事件:
flow { throw RuntimeException ( " broken! " ) }.test {
assertEquals( " broken! " , awaitError().message)
}如果不消耗錯誤將導致與上述相同的未消耗事件異常,但是除例外是原因,以便可以使用完整的stacktrace。
flow< Nothing > { throw RuntimeException ( " broken! " ) }.test { } app.cash.turbine.TurbineAssertionError: Unconsumed events found:
- Error(RuntimeException)
at app//app.cash.turbine.ChannelTurbine.ensureAllEventsConsumed(Turbine.kt:215)
... 80 more
Caused by: java.lang.RuntimeException: broken!
at example.MainKt$main$1.invokeSuspend(FlowTest.kt:652)
... 105 more
除了從流量創建的應ReceiveTurbine器外,獨立的Turbine機還可用於與流程外的測試代碼進行通信。到處使用它們,您可能再也不需要runCurrent()了。這是如何在假貨中使用Turbine()的示例:
class FakeNavigator : Navigator {
val goTos = Turbine < Screen >()
override fun goTo ( screen : Screen ) {
goTos.add(screen)
}
}runTest {
val navigator = FakeNavigator ()
val events : Flow < UiEvent > =
MutableSharedFlow < UiEvent >(extraBufferCapacity = 50 )
val models : Flow < UiModel > =
makePresenter(navigator).present(events)
models.test {
assertEquals( UiModel (title = " Hi there " ), awaitItem())
events.emit( UiEvent . Close )
assertEquals( Screens . Back , navigator.goTos.awaitItem())
}
}為了支持COROUTINES和非核心代碼的混合代碼庫,獨立的Turbine包括非懸浮的兼容兼式API。所有await方法都有相當於無懸的方法take方法:
val navigator = FakeNavigator ()
val events : PublishRelay < UiEvent > = PublishRelay .create()
val models : Observable < UiModel > =
makePresenter(navigator).present(events)
val testObserver = models.test()
testObserver.assertValue( UiModel (title = " Hi there " ))
events.accept( UiEvent . Close )
assertEquals( Screens . Back , navigator.goTos.takeItem())使用takeItem()和朋友, Turbine行為就像簡單的隊列;使用awaitItem()和朋友,它是Turbine 。
這些方法只能在非懸而未決的上下文中使用。在JVM平台上,它們將在懸浮上下文中使用。
默認情況下,流是異步的。您的流量是通過渦輪在測試代碼旁邊同時收集的。
處理這種異步性與渦輪機的作用與生產Coroutines代碼相同:而不是使用runCurrent()之類的工具來“按”沿著異步流的“推動”,而是使用Turbine機( Turbine )的awaitItem() ,等待complete(),而awaitComplete()和awaitError() ”將它們拉開,然後停車,直到準備好了。
channelFlow {
withContext( IO ) {
Thread .sleep( 100 )
send( " item " )
}
}.test {
assertEquals( " item " , awaitItem())
awaitComplete()
}您的驗證代碼可能與測試的流量同時運行,但渦輪機將其盡可能地放在駕駛員座椅上: test將在執行驗證塊時結束,隱含地取消測試的流量。
channelFlow {
withContext( IO ) {
repeat( 10 ) {
Thread .sleep( 200 )
send( " item $it " )
}
}
}.test {
assertEquals( " item 0 " , awaitItem())
assertEquals( " item 1 " , awaitItem())
assertEquals( " item 2 " , awaitItem())
}在任何時候也可以明確取消流量。
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())
}可以命名渦輪機以改善錯誤反饋。將name傳遞到test , testIn或Turbine() ,並將其包含在被丟棄的任何錯誤中:
runTest {
turbineScope {
val turbine1 = flowOf( 1 ).testIn(backgroundScope, name = " turbine 1 " )
val turbine2 = flowOf( 2 ).testIn(backgroundScope, name = " turbine 2 " )
turbine1.awaitComplete()
turbine2.awaitComplete()
}
} Expected complete for turbine 1 but found Item(1)
app.cash.turbine.TurbineAssertionError: Expected complete for turbine 1 but found Item(1)
at app//app.cash.turbine.ChannelKt.unexpectedEvent(channel.kt:258)
at app//app.cash.turbine.ChannelKt.awaitComplete(channel.kt:226)
at app//app.cash.turbine.ChannelKt$awaitComplete$1.invokeSuspend(channel.kt)
at app//kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
...
共享的流對執行順序很敏感。在調用collect之前致電emit將刪除發射值:
val mutableSharedFlow = MutableSharedFlow < Int >(replay = 0 )
mutableSharedFlow.emit( 1 )
mutableSharedFlow.test {
assertEquals(awaitItem(), 1 )
} No value produced in 1s
java.lang.AssertionError: No value produced in 1s
at app.cash.turbine.ChannelKt.awaitEvent(channel.kt:90)
at app.cash.turbine.ChannelKt$awaitEvent$1.invokeSuspend(channel.kt)
(Coroutine boundary)
at kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTestCoroutine$2.invokeSuspend(TestBuilders.kt:212)
渦輪機的test和testIn方法可以確保在進行前測試的流量將升至第一個懸架點。因此,在排放之前對共享流進行調用test不會下降:
val mutableSharedFlow = MutableSharedFlow < Int >(replay = 0 )
mutableSharedFlow.test {
mutableSharedFlow.emit( 1 )
assertEquals(awaitItem(), 1 )
}如果您的代碼在共享流程中收集,請確保它會迅速獲得可愛的體驗。
Kotlin當前提供的共享流類型類型是:
MutableStateFlowStateFlowMutableSharedFlowSharedFlow每當等待事件時,渦輪機都會應用超時。這是一個壁時鐘時間超時,忽略了runTest的虛擬時鐘時間。
默認超時長度為三秒鐘。可以通過將超時持續時間進行test來覆蓋:
flowOf( " one " , " two " ).test(timeout = 10 .milliseconds) {
.. .
}此超時將用於驗證塊內的所有與渦輪機相關的調用。
您還可以覆蓋用testIn和Turbine()創建的渦輪機的超時:
val standalone = Turbine < String >(timeout = 10 .milliseconds)
val flow = flowOf( " one " ).testIn(
scope = backgroundScope,
timeout = 10 .milliseconds,
)這些超時僅適用於施用它們的Turbine 。
最後,您還可以使用withTurbineTimeout out更改整個代碼的超時:
withTurbineTimeout( 10 .milliseconds) {
.. .
}渦輪機的大部分API都作為Channel上的擴展實現。 Turbine的API表面更有限,通常是可取的,但是如果您需要,這些擴展也可以作為公共API提供。
Copyright 2018 Square, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.