터빈은 Kotlinx의 작은 테스트 라이브러리입니다. 코 루틴 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 " )
} Turbine의 자체 API는 안정적이지만 현재 Kotlinx의 불안정한 API에 의존해야합니다. Coroutines Test Artifact : UnconfinedTestDispatcher . 이 터빈을 사용하지 않으면 runTest 중단됩니다. 향후 코 루틴 라이브러리 업데이트 가이 라이브러리의 동작을 결과로 변경할 수 있습니다. 우리는이 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 새로운 코 루틴을 시작하고 someFlow.collect 호출하고 결과를 Turbine 으로 공급합니다. 그런 다음 유효성 검사 블록을 호출하여 읽기 전용 ReceiveTurbine 인터페이스를 수신자로 전달합니다.
flowOf( " one " ).test {
assertEquals( " one " , awaitItem())
awaitComplete()
} 유효성 검사 블록이 완료되면 test 코 루틴을 취소하고 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() 호출됩니다.
testIn 코 루틴을 자동으로 정리할 수 없으므로 러닝 흐름이 끝나는지 확인해야합니다. 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 S를 사용하여 흐름 외부의 테스트 코드와 통신 할 수 있습니다. 어디서나 사용하면 다시는 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 비소 보급 콤프 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 플랫폼에서는 정지 컨텍스트에서 사용될 때 던질 것입니다.
흐름은 기본적으로 비동기식입니다. 귀하의 흐름은 테스트 코드와 함께 터빈으로 동시에 수집됩니다.
이 비동기식을 처리하는 awaitItem() 프로덕션 코 루틴 코드에서와 awaitComplete() 터빈 Turbine 동일한 방식 awaitError() runCurrent() 합니다. 새로운 이벤트가 준비 될 때까지 주차하여 "그들을 잡아 당깁니다.
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())
} 오류 피드백을 향상시키기 위해 터빈 이름을 지정할 수 있습니다. test , testIn 또는 Turbine() 으로 name 전달하면 발생하는 오류에 포함됩니다.
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 의 가상 시계 시간을 무시하는 Wall Clock Time Timeout입니다.
기본 시간 초과 길이는 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.