A turbina é uma pequena biblioteca de testes para Kotlinx.Coroutines Flow .
flowOf( " one " , " two " ).test {
assertEquals( " one " , awaitItem())
assertEquals( " two " , awaitItem())
awaitComplete()
}Uma turbina é um dispositivo mecânico rotativo que extrai energia de um fluxo de fluido e o converte em um trabalho ú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 " )
} Embora a própria API da Turbine seja estável, atualmente somos forçados a depender de uma API instável da Kotlinx.Coroutines Test Artifact: UnconfinedTestDispatcher . Sem esse uso de turbina com runTest quebraria. É possível para futuras atualizações da Biblioteca Coroutina para alterar o comportamento dessa biblioteca como resultado. Faremos todos os esforços para garantir a estabilidade comportamental também até que essa dependência da API seja estabilizada (rastreando a edição 132).
Uma Turbine é um invólucro fino sobre um Channel com uma API projetada para teste.
Você pode ligar awaitItem() para suspender e esperar que um item seja enviado para a Turbine :
assertEquals( " one " , turbine.awaitItem()) ... awaitComplete() para suspender até que a Turbine seja concluída sem exceção:
turbine.awaitComplete() ... ou awaitError() para suspender até que a Turbine seja concluída com um Throwable .
assertEquals( " broken! " , turbine.awaitError().message) Se await* é chamado e nada acontece, Turbine vai tempo limite e falhará em vez de pendurar.
Quando terminar de uma Turbine , você pode limpar ligando para cancel() para encerrar quaisquer coroutinas de apoio. Finalmente, você pode afirmar que todos os eventos foram consumidos chamando ensureAllEventsConsumed() .
A maneira mais simples de criar e executar uma Turbine é produzir uma a partir de um Flow . Para testar um único Flow , chame a extensão test :
someFlow.test {
// Validation code here!
} test lança uma nova coroutina, chama someFlow.collect e alimenta os resultados em uma Turbine . Em seguida, ele chama o bloco de validação, passando na interface ReceiveTurbine somente leitura como receptor:
flowOf( " one " ).test {
assertEquals( " one " , awaitItem())
awaitComplete()
} Quando o bloco de validação estiver concluído, test cancela a coroutina e as chamadas ensureAllEventsConsumed() .
Para testar vários fluxos, atribua cada Turbine a um val separado chamando testIn :
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()
}
} Assim como test , testIn produz uma ReceiveTurbine . ensureAllEventsConsumed() será invocado quando a corotação de chamadas for concluída.
testIn não pode limpar automaticamente sua coroutina, por isso cabe a você garantir que o fluxo em execução termine. Use runTest 's backgroundScope e ele cuidará disso automaticamente. Caso contrário, certifique -se de ligar para um dos seguintes métodos antes do final do seu escopo:
cancel()awaitComplete()awaitError()Caso contrário, seu teste ficará pendurado.
Não consumir todos os eventos antes do final do bloco de validação de uma Turbine baseado em fluxo falhará no seu teste:
flowOf( " one " , " two " ).test {
assertEquals( " one " , awaitItem())
} Exception in thread "main" AssertionError:
Unconsumed events found:
- Item(two)
- Complete
O mesmo vale para testIn , mas no final da 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
Eventos recebidos podem ser explicitamente ignorados, no entanto.
flowOf( " one " , " two " ).test {
assertEquals( " one " , awaitItem())
cancelAndIgnoreRemainingEvents()
}Além disso, podemos receber o item emitido mais recente e ignorar os 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()
} Os eventos de terminação de fluxo (exceções e conclusão) são expostos como eventos que devem ser consumidos para validação. Assim, por exemplo, lançar uma RuntimeException dentro do seu flow não lançará uma exceção em seu teste. Em vez disso, produzirá um evento de erro da turbina:
flow { throw RuntimeException ( " broken! " ) }.test {
assertEquals( " broken! " , awaitError().message)
}A falha em consumir um erro resultará na mesma exceção de eventos não consumidos que acima, mas com a exceção adicionada como a causa, para que o pilheiro completo esteja disponível.
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
Além da ReceiveTurbine S criada a partir de fluxos, Turbine independentes podem ser usadas para se comunicar com o código de teste fora de um fluxo. Use -os em todos os lugares, e você pode nunca mais precisar de runCurrent() . Aqui está um exemplo de como usar Turbine() em uma farsa:
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 suportar bases de código com uma mistura de coroutinas e código não coroutinas, Turbine independente inclui APIs de compat não suspensas. Todos os métodos await têm métodos take equivalentes que não são suspensos:
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() e amigos, e Turbine se comporta como uma fila simples; Use awaitItem() e amigos, e é uma Turbine .
Esses métodos devem ser usados apenas a partir de um contexto não suspenso. Nas plataformas JVM, elas lançarão quando usadas em um contexto de suspensão.
Os fluxos são assíncronos por padrão. Seu fluxo é coletado simultaneamente por turbina ao lado do código de teste.
O manuseio dessa assincronicidade funciona da mesma maneira com a turbina do código de corotas de produção: em vez de usar ferramentas como runCurrent() para "empurrar" um fluxo assíncrono, Turbine 's's awaitItem() , awaitComplete() e awaitError() " Puxe -os por estacionar até que um novo evento esteja pronto.
channelFlow {
withContext( IO ) {
Thread .sleep( 100 )
send( " item " )
}
}.test {
assertEquals( " item " , awaitItem())
awaitComplete()
} Seu código de validação pode ser executado simultaneamente com o fluxo em teste, mas a turbina o coloca no banco do motorista o máximo possível: test terminará quando o seu bloco de validação for executado, cancelando implicitamente o fluxo em teste.
channelFlow {
withContext( IO ) {
repeat( 10 ) {
Thread .sleep( 200 )
send( " item $it " )
}
}
}.test {
assertEquals( " item 0 " , awaitItem())
assertEquals( " item 1 " , awaitItem())
assertEquals( " item 2 " , awaitItem())
}Os fluxos também podem ser explicitamente cancelados a qualquer 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())
} As turbinas podem ser nomeadas para melhorar o feedback de erros. Passe em um name para test , testIn ou Turbine() , e será incluído em quaisquer erros que sejam lançados:
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)
...
Os fluxos compartilhados são sensíveis à ordem de execução. Chamando emit antes de ligar para collect soltará o 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)
Os métodos test e testIn da turbina garantem que o fluxo em teste suba até o primeiro ponto de suspensão antes de prosseguir. Portanto, test de chamada em um fluxo compartilhado antes de emitir não cairá:
val mutableSharedFlow = MutableSharedFlow < Int >(replay = 0 )
mutableSharedFlow.test {
mutableSharedFlow.emit( 1 )
assertEquals(awaitItem(), 1 )
}Se o seu código coletar em fluxos compartilhados, verifique se ele prontamente, para ter uma experiência adorável.
Os tipos de fluxo compartilhados que Kotlin fornece atualmente são:
MutableStateFlowStateFlowMutableSharedFlowSharedFlow A turbina aplica um tempo limite sempre que aguarda um evento. Este é um tempo de tempo de relógio de parede que ignora o tempo de relógio virtual do runTest .
O comprimento do tempo limite padrão é de três segundos. Isso pode ser substituído passando uma duração de tempo limite para test :
flowOf( " one " , " two " ).test(timeout = 10 .milliseconds) {
.. .
}Esse tempo limite será usado para todas as chamadas relacionadas à turbina dentro do bloco de validação.
Você também pode substituir o tempo limite para turbinas criadas com testIn e Turbine() :
val standalone = Turbine < String >(timeout = 10 .milliseconds)
val flow = flowOf( " one " ).testIn(
scope = backgroundScope,
timeout = 10 .milliseconds,
) Esses substitutos de tempo limite se aplicam apenas à Turbine na qual foram aplicados.
Por fim, você também pode alterar o tempo limite para um bloco inteiro de código usando withTurbineTimeout :
withTurbineTimeout( 10 .milliseconds) {
.. .
} A maioria das APIs da turbina é implementada como extensões no Channel . A superfície da API mais limitada da Turbine é geralmente preferível, mas essas extensões também estão disponíveis como APIs públicas, se você precisar delas.
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.