Turbine ist eine kleine Testbibliothek für Kotlinx.Coroutines Flow .
flowOf( " one " , " two " ).test {
assertEquals( " one " , awaitItem())
assertEquals( " two " , awaitItem())
awaitComplete()
}Eine Turbine ist eine rotierende mechanische Vorrichtung, die Energie aus einem Flüssigkeitsstrom extrahiert und sie in nützliche Arbeit umwandelt.
- Wikipedia
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 " )
} Während Turbines eigene API stabil ist, sind wir derzeit gezwungen UnconfinedTestDispatcher auf eine instabile API von kotlinx zu verlassen. Ohne diese Verwendung von Turbinen mit runTest würde es brechen. Es ist möglich, dass zukünftige Updates für die Coroutine -Bibliothek das Verhalten dieser Bibliothek als Ergebnis verändern. Wir werden alle Anstrengungen unternehmen, um die Verhaltensstabilität zu gewährleisten, bis diese API -Abhängigkeit stabilisiert ist (Tracking -Ausgabe Nr. 132).
Eine Turbine ist eine dünne Wrapper über einem Channel mit einer API, die zum Testen ausgelegt ist.
Sie können awaitItem() anrufen, um auszusetzen und darauf zu warten, dass ein Artikel an die Turbine gesendet wird:
assertEquals( " one " , turbine.awaitItem()) ... awaitComplete() bis die Turbine ohne Ausnahme abgeschlossen ist:
turbine.awaitComplete() ... oder awaitError() , um zu suspendieren, bis die Turbine mit einem Throwable abgeschlossen ist.
assertEquals( " broken! " , turbine.awaitError().message) Wenn await* genannt und nichts passiert, Turbine wird ausschließlich ausgerichtet und scheitern, anstatt zu hängen.
Wenn Sie mit einer Turbine fertig sind, können Sie aufräumen, indem Sie cancel() anrufen, um alle Coroutinen zu beenden. Schließlich können Sie behaupten, dass alle Ereignisse konsumiert wurden, indem Sie ensureAllEventsConsumed() anrufen.
Der einfachste Weg, eine Turbine zu erstellen und zu führen, besteht darin, einen aus einem Flow zu produzieren. Um einen einzelnen Flow zu testen, rufen Sie die test auf:
someFlow.test {
// Validation code here!
} test startet eine neue Coroutine, ruft someFlow.collect auf und führt die Ergebnisse in eine Turbine . Anschließend ruft er den Validierungsblock auf und gibt die schreibgeschützte ReceiveTurbine als Empfänger ein:
flowOf( " one " ).test {
assertEquals( " one " , awaitItem())
awaitComplete()
} Wenn der Validierungsblock abgeschlossen ist, storniert test die Coroutine und Anrufe ensureAllEventsConsumed() .
Um mehrere Ströme zu testen, weisen Sie jede Turbine einem separaten val zu, indem Sie stattdessen testIn aufrufen:
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()
}
} Wie bei test erzeugt testIn eine ReceiveTurbine . ensureAllEventsConsumed() wird aufgerufen, wenn die Coroutine abgeschlossen ist.
testIn kann seine Coroutine nicht automatisch aufräumen, daher liegt es an Ihnen, sicherzustellen, dass der Lauffluss endet. Verwenden Sie backgroundScope von runTest , und dies wird sich automatisch darum kümmern. Rufen Sie ansonsten sicher, dass Sie vor dem Ende Ihres Bereichs eine der folgenden Methoden anrufen:
cancel()awaitComplete()awaitError()Andernfalls hängt Ihr Test.
Wenn Sie nicht alle Ereignisse vor dem Ende des Validierungsblocks eines fließbasierten Turbine konsumieren, fehlgefügt Ihr Test:
flowOf( " one " , " two " ).test {
assertEquals( " one " , awaitItem())
} Exception in thread "main" AssertionError:
Unconsumed events found:
- Item(two)
- Complete
Gleiches gilt für testIn , aber am Ende der Coroutine:
runTest {
turbineScope {
val turbine = flowOf( " one " , " two " ).testIn(backgroundScope)
turbine.assertEquals( " one " , awaitItem())
}
} Exception in thread "main" AssertionError:
Unconsumed events found:
- Item(two)
- Complete
Empfangene Ereignisse können jedoch ausdrücklich ignoriert werden.
flowOf( " one " , " two " ).test {
assertEquals( " one " , awaitItem())
cancelAndIgnoreRemainingEvents()
}Darüber hinaus können wir den neuesten emittierten Artikel erhalten und die vorherigen ignorieren.
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()
} Ereignisse der Durchflussabschlüsse (Ausnahmen und Abschluss) werden als Ereignisse freigelegt, die zur Validierung konsumiert werden müssen. Wenn Sie beispielsweise eine RuntimeException in Ihren flow werfen, wird Sie in Ihrem Test keine Ausnahme auswirken. Stattdessen erzeugt es ein Turbinenfehlerereignis:
flow { throw RuntimeException ( " broken! " ) }.test {
assertEquals( " broken! " , awaitError().message)
}Wenn Sie einen Fehler nicht konsumieren, führt dies zu derselben nicht ausgezeichneten Ereignisausnahme wie oben. Mit der Ausnahme, die als Ursache hinzugefügt wird, so dass die vollständige Stacktrace verfügbar ist.
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
Zusätzlich zu den aus Strömungen erzeugten ReceiveTurbine können eigenständige Turbine verwendet werden, um mit dem Testcode außerhalb eines Flusses zu kommunizieren. Verwenden Sie sie überall, und Sie brauchen vielleicht nie wieder runCurrent() . Hier ist ein Beispiel für die Verwendung Turbine() in einer Fälschung:
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())
}
} Um Codebasen mit einer Mischung aus Coroutinen und Nicht-Coroutiner-Code zu unterstützen, umfasst eigenständige Turbine nicht ausgewählte Compat-APIs. Alle auf await Methoden haben gleichwertige take , die nicht ausgewählt werden:
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()) Verwenden Sie takeItem() und Freunde, und Turbine verhält sich wie einfache Warteschlange; Verwenden Sie awaitItem() und Freunde, und es ist eine Turbine .
Diese Methoden sollten nur aus einem nicht spendenden Kontext verwendet werden. Auf JVM -Plattformen werden sie aus einem suspendierenden Kontext geworfen.
Die Flüsse sind standardmäßig asynchron. Ihr Fluss wird gleichzeitig durch Turbine zusammen mit Ihrem Testcode gesammelt.
Die Behandlung dieser Asynchronizität funktioniert auf die gleiche Weise wie in der Turbine wie in der Produktion Coroutines Code: Anstatt Tools wie runCurrent() zu verwenden, um einen asynchronen Fluss entlang zu drücken, Turbine awaitItem() , awaitComplete() und awaitError() " Ziehen Sie sie mit dem Parken, bis eine neue Veranstaltung fertig ist.
channelFlow {
withContext( IO ) {
Thread .sleep( 100 )
send( " item " )
}
}.test {
assertEquals( " item " , awaitItem())
awaitComplete()
} Ihr Validierungscode kann gleichzeitig mit dem zu testenden Fluss ausgeführt werden, aber Turbine stellt ihn so weit wie möglich auf den Fahrersitz ein: test endet, wenn Ihr Validierungsblock ausgeführt wird, wodurch der untersuchte Fluss implizit abgesagt wird.
channelFlow {
withContext( IO ) {
repeat( 10 ) {
Thread .sleep( 200 )
send( " item $it " )
}
}
}.test {
assertEquals( " item 0 " , awaitItem())
assertEquals( " item 1 " , awaitItem())
assertEquals( " item 2 " , awaitItem())
}Flows können zu jedem Zeitpunkt auch explizit abgesagt werden.
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())
} Turbinen können benannt werden, um das Fehlerfeedback zu verbessern. Geben Sie einen name zum test , testIn oder Turbine() über, und er wird in alle Fehler enthalten, die geworfen werden:
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)
...
Shared Flows reagieren empfindlich gegenüber der Ausführungsreihenfolge. Das Rufen emit vor dem Anrufe collect wird den emittierten Wert fallen:
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 und testIn -Methoden von Turbine garantieren, dass der zu testende Durchfluss vor dem Fortfahren bis zum ersten Suspensionspunkt läuft. Wenn Sie test auf einem gemeinsamen Fluss vor dem Abgeben aufrufen, wird dies nicht sinken:
val mutableSharedFlow = MutableSharedFlow < Int >(replay = 0 )
mutableSharedFlow.test {
mutableSharedFlow.emit( 1 )
assertEquals(awaitItem(), 1 )
}Wenn Ihr Code auf gemeinsamen Flüssen sammelt, stellen Sie sicher, dass dies sofort eine schöne Erfahrung hat.
Die gemeinsamen Flusstypen, die Kotlin derzeit bereitstellt, sind:
MutableStateFlowStateFlowMutableSharedFlowSharedFlow Turbine wendet eine Zeitüberschreitung an, wenn sie auf ein Ereignis wartet. Dies ist eine Zeitüberschreitung der Wanduhr, die die virtuelle Uhrzeit von runTest ignoriert.
Die Standardzeitlänge beträgt drei Sekunden. Dies kann überschrieben werden, indem eine Zeitüberschreitungsdauer zum test bestanden wird:
flowOf( " one " , " two " ).test(timeout = 10 .milliseconds) {
.. .
}Diese Zeitüberschreitung wird für alle turbinenbezogenen Anrufe innerhalb des Validierungsblocks verwendet.
Sie können auch die Zeitüberschreitung für Turbinen überschreiben, die mit testIn und Turbine() erstellt wurden:
val standalone = Turbine < String >(timeout = 10 .milliseconds)
val flow = flowOf( " one " ).testIn(
scope = backgroundScope,
timeout = 10 .milliseconds,
) Diese Zeitüberschreitungen gelten nur für die Turbine , auf die sie angewendet wurden.
Schließlich können Sie auch die Zeitüberschreitung für einen ganzen Codeblock mit withTurbineTimeout ändern:
withTurbineTimeout( 10 .milliseconds) {
.. .
} Die meisten APIs von Turbinen werden als Erweiterungen auf Channel implementiert. Die begrenztere API Turbine ist normalerweise vorzuziehen, diese Erweiterungen sind jedoch auch als öffentliche APIs verfügbar, wenn Sie sie benötigen.
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.