La turbina es una pequeña biblioteca de pruebas para Flow Kotlinx.Coroutines.
flowOf( " one " , " two " ).test {
assertEquals( " one " , awaitItem())
assertEquals( " two " , awaitItem())
awaitComplete()
}Una turbina es un dispositivo mecánico giratorio que extrae energía de un flujo de fluido y la convierte en un trabajo útil.
- 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 " )
} Si bien la propia API de Turbine es estable, actualmente nos vemos obligados a depender de una API inestable de Kotlinx.Coroutines Test Artifact: UnconfinedTestDispatcher . Sin este uso de turbina con runTest se rompería. Es posible que las futuras actualizaciones de la biblioteca de Coroutine alteren el comportamiento de esta biblioteca como resultado. Haremos todo lo posible para garantizar la estabilidad del comportamiento también hasta que esta dependencia de la API se estabilice (el problema de seguimiento #132).
Una Turbine es una envoltura delgada sobre un Channel con una API diseñada para la prueba.
Puede llamar a awaitItem() para suspender y esperar a que se envíe un artículo a la Turbine :
assertEquals( " one " , turbine.awaitItem()) ... awaitComplete() para suspender hasta que la Turbine se complete sin una excepción:
turbine.awaitComplete() ... o awaitError() para suspender hasta que la Turbine se complete con un Throwable .
assertEquals( " broken! " , turbine.awaitError().message) Si await* se llama y no sucede nada, Turbine se agota y fallará en lugar de colgar.
Cuando haya terminado con una Turbine , puede limpiar llamando cancel() para terminar cualquier coroutina de respaldo. Finalmente, puede afirmar que todos los eventos se consumieron llamando ensureAllEventsConsumed() .
La forma más sencilla de crear y ejecutar una Turbine es producir una de un Flow . Para probar un solo Flow , llame a la extensión test :
someFlow.test {
// Validation code here!
} test lanza una nueva coroutine, llama a someFlow.collect y alimenta los resultados a una Turbine . Luego llama al bloque de validación, pasando a la interfaz ReceiveTurbine de solo lectura como receptor:
flowOf( " one " ).test {
assertEquals( " one " , awaitItem())
awaitComplete()
} Cuando se completa el bloque de validación, test cancela la coroutina y llama ensureAllEventsConsumed() .
Para probar múltiples flujos, asigne cada Turbine a un val separado llamando testIn en su lugar:
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()
}
} Al igual que test , testIn produce una ReceiveTurbine . ensureAllEventsConsumed() se invocará cuando se complete el Coroutine de llamadas.
testIn no puede limpiar automáticamente su coroutina, por lo que depende de usted asegurarse de que el flujo de ejecución termine. Use backgroundScope de runTest , y se encargará de esto automáticamente. De lo contrario, asegúrese de llamar a uno de los siguientes métodos antes del final de su alcance:
cancel()awaitComplete()awaitError()De lo contrario, su prueba se colgará.
No consumir todos los eventos antes del final de un bloque de validación de una Turbine basado en el flujo fallará en su prueba:
flowOf( " one " , " two " ).test {
assertEquals( " one " , awaitItem())
} Exception in thread "main" AssertionError:
Unconsumed events found:
- Item(two)
- Complete
Lo mismo ocurre con testIn , pero al final de Calling 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
Sin embargo, los eventos recibidos pueden ignorarse explícitamente.
flowOf( " one " , " two " ).test {
assertEquals( " one " , awaitItem())
cancelAndIgnoreRemainingEvents()
}Además, podemos recibir el artículo emitido más reciente e ignorar los anteriores.
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()
} Los eventos de terminación de flujo (excepciones y finalización) están expuestos como eventos que deben consumirse para la validación. Entonces, por ejemplo, lanzar una RuntimeException dentro de su flow no lanzará una excepción en su prueba. En su lugar, producirá un evento de error de turbina:
flow { throw RuntimeException ( " broken! " ) }.test {
assertEquals( " broken! " , awaitError().message)
}No consumir un error dará como resultado la misma excepción de eventos inconsumuladas que anteriormente, pero con la excepción agregada como causa para que la StackTrace completa esté disponible.
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
Además de ReceiveTurbine creada a partir de flujos, se pueden usar Turbine independientes para comunicarse con el código de prueba fuera de un flujo. Úselos en todas partes, y es posible que nunca necesite runCurrent() nuevamente. Aquí hay un ejemplo de cómo usar Turbine() en una falsa:
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())
}
} Para admitir bases de código con una combinación de coroutinas y código no coroutinas, Turbine independiente incluye API no superpendientes de compats. Todos los métodos await tienen métodos take equivalentes que no son susceptibles:
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()) Use takeItem() y amigos, y Turbine se comporta como una simple cola; Use awaitItem() y sus amigos, y es una Turbine .
Estos métodos solo deben usarse a partir de un contexto no suspendido. En las plataformas JVM, lanzarán cuando se usen desde un contexto de suspensión.
Los flujos son asíncronos de forma predeterminada. Su flujo es recolectado simultáneamente por la turbina junto con su código de prueba.
El manejo de esta asincronicidad funciona de la misma manera con la turbina que en el código de la coroutina de producción: en lugar de usar herramientas como runCurrent() para "empujar" un flujo asíncrono a lo largo, Turbine awaitItem() , awaitComplete() y awaitError() " Tire de ellos al estacionar hasta que un nuevo evento esté listo.
channelFlow {
withContext( IO ) {
Thread .sleep( 100 )
send( " item " )
}
}.test {
assertEquals( " item " , awaitItem())
awaitComplete()
} Su código de validación puede ejecutarse simultáneamente con el flujo bajo prueba, pero la turbina lo coloca en el asiento del conductor tanto como sea posible: test finalizará cuando su bloque de validación se realice, cancelando implícitamente el flujo bajo prueba.
channelFlow {
withContext( IO ) {
repeat( 10 ) {
Thread .sleep( 200 )
send( " item $it " )
}
}
}.test {
assertEquals( " item 0 " , awaitItem())
assertEquals( " item 1 " , awaitItem())
assertEquals( " item 2 " , awaitItem())
}Los flujos también se pueden cancelar explícitamente en cualquier momento.
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())
} Las turbinas pueden ser nombradas para mejorar la retroalimentación de errores. Pase en un name para test , testIn o Turbine() , y se incluirá en cualquier error que se lance:
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)
...
Los flujos compartidos son sensibles al orden de ejecución. Llamar emit antes de llamar collect dejará caer el valor emitido:
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)
Los métodos de test de turbina y testIn garantizan que el flujo bajo la prueba se ejecutará hasta el primer punto de suspensión antes de continuar. Entonces, llamar a test en un flujo compartido antes de emitir no caerá:
val mutableSharedFlow = MutableSharedFlow < Int >(replay = 0 )
mutableSharedFlow.test {
mutableSharedFlow.emit( 1 )
assertEquals(awaitItem(), 1 )
}Si su código se recopila en flujos compartidos, asegúrese de que lo haga de inmediato tener una experiencia encantadora.
Los tipos de flujo compartido que Kotlin proporciona actualmente son:
MutableStateFlowStateFlowMutableSharedFlowSharedFlow La turbina aplica un tiempo de espera cada vez que espera un evento. Este es un tiempo de espera de tiempo de reloj de pared que ignora el tiempo de reloj virtual de runTest .
La longitud de tiempo de espera predeterminada es de tres segundos. Esto se puede anular pasando una duración de tiempo de espera para test :
flowOf( " one " , " two " ).test(timeout = 10 .milliseconds) {
.. .
}Este tiempo de espera se utilizará para todas las llamadas relacionadas con la turbina dentro del bloque de validación.
También puede anular el tiempo de espera de las turbinas creadas con testIn y Turbine() :
val standalone = Turbine < String >(timeout = 10 .milliseconds)
val flow = flowOf( " one " ).testIn(
scope = backgroundScope,
timeout = 10 .milliseconds,
) Estas anulaciones de tiempo de espera solo se aplican a la Turbine en la que se aplicaron.
Finalmente, también puede cambiar el tiempo de espera para un bloque completo de código utilizando withTurbineTimeout :
withTurbineTimeout( 10 .milliseconds) {
.. .
} La mayoría de las API de Turbine se implementan como extensiones en Channel . La superficie API más limitada de Turbine generalmente es preferible, pero estas extensiones también están disponibles como API públicas si las necesita.
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.