Турбина - это небольшая библиотека тестирования для Flow kotlinx.coroutines.
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 Turbine является стабильным, в настоящее время мы вынуждены зависеть от нестабильного API от kotlinx.coroutines test artifact: UnconfinedTestDispatcher . Без этого использования турбины с runTest сломается. Для будущих обновлений библиотеки могут изменить поведение этой библиотеки в результате. Мы также приложим все усилия для обеспечения поведенческой стабильности, пока эта зависимость 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() .
Чтобы проверить несколько потоков, назначьте каждую Turbine отдельной val , вызовите 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()
}
} Как и test , testIn производит ReceiveTurbine . ensureAllEventsConsumed() будет вызвана, когда завершается Calloutine.
testIn не может автоматически очистить свою цифру, поэтому вам решать, что текущий поток заканчивается. Используйте backgroundScope от runTest , и он позаботится об этом автоматически. В противном случае, обязательно вызовите один из следующих методов перед окончанием вашей области:
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()
} События завершения потока (исключения и завершение) представлены как события, которые должны быть использованы для проверки. Так, например, бросание RuntimeException внутри вашего flow не вызовет исключение в вашем тесте. Вместо этого он будет создавать событие ошибки турбины:
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())
}
} Чтобы поддержать кодовые базы сочетанием Corootines и не-кораутинов, автономная Turbine включает API COMPAT без SUSPAT. Все методы await имеют эквивалентные методы take , которые не являются SUSPENDING:
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 они будут бросаться при использовании из приостановки контекста.
Потоки асинхронны по умолчанию. Ваш поток собирается одновременно турбиной наряду с вашим тестовым кодом.
Обработка этой асинхронности работает так же с турбиной, как это происходит в коде производства CODINES: вместо использования таких инструментов, как runCurrent() , чтобы «подтолкнуть» асинхронный поток вдоль, Turbine 's awaitItem() , awaitComplete() и awaitError() » Потяните их на парковку, пока новое событие не будет готово.
channelFlow {
withContext( IO ) {
Thread .sleep( 100 )
send( " item " )
}
}.test {
assertEquals( " item " , awaitItem())
awaitComplete()
} Ваш код проверки может работать одновременно с тестированием потока, но Turbine помещает его на сиденье водителя как можно больше: 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)
...
Общие потоки чувствительны к порядку исполнения. Вызовы emit перед вызовом collect отбросит испускаемое значение:
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 :
withTurbineTimeout( 10 .milliseconds) {
.. .
} Большинство API -интерфейсов Turbine реализованы как расширения на Channel . Более ограниченная поверхность API Turbine обычно предпочтительнее, но эти расширения также доступны в качестве общедоступных 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.