タービンは、kotlinx.coroutines 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.coroutinesテストアーティファクトからの不安定なAPI: UnconfinedTestDispatcherに依存することを余儀なくされています。このタービンの使用がないと、 runTestが使用されないと壊れます。結果として、将来のCoroutine Libraryの更新がこのライブラリの動作を変更する可能性があります。このAPI依存関係が安定するまで、行動の安定性を確保するためにあらゆる努力をします(追跡問題#132)。
Turbine 、テスト用に設計されたAPIを備えたChannel上の薄いラッパーです。
awaitItem()に電話して、アイテムがTurbineに送信されるのを一時停止して待ちます。
assertEquals( " one " , turbine.awaitItem()) ...例外なくTurbineが完了するまで、懸濁awaitComplete() 。
turbine.awaitComplete() ...または、 Turbine Throwableで完了するまで停止するのをawaitError() 。
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を生成します。 ensureAllEventsConsumed()呼び出しのCoroutineが完了すると呼び出されます。
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)
}エラーを消費しないと、上記と同じ未発表のイベント例外が発生しますが、例外が原因として追加されているため、完全なスタックトレースが利用可能になります。
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())
}
}コードベースとコルーチン以外のコードを組み合わせたコードベースをサポートするために、スタンドアロンTurbineには、懸濁していないcompat 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プラットフォームでは、中断コンテキストから使用するとスローされます。
フローはデフォルトでは非同期です。あなたのフローは、テストコードとともにタービンによって同時に収集されます。
この非同期性の取り扱いは、タービンの生産コードと同じように同じように機能します。runcurrent runCurrent()などのツールを使用して非同期フローを「プッシュ」する代わりに、 TurbineのawaitItem() 、 awaitComplete() 、and 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の仮想クロック時間を無視するウォールクロックタイムアウトです。
デフォルトのタイムアウトの長さは3秒です。これは、タイムアウト期間を通過して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を使用して、コードのブロック全体のタイムアウトを変更することもできます。
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.