กังหันเป็นไลบรารีการทดสอบขนาดเล็กสำหรับ kotlinx.coroutines 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 " )
} ในขณะที่ API ของ Turbine มีความเสถียร แต่ขณะนี้เราถูกบังคับให้ต้องพึ่งพา API ที่ไม่เสถียรจาก Kotlinx.coroutines สิ่งประดิษฐ์ทดสอบ: UnconfinedTestDispatcher หากไม่มีการใช้กังหันด้วย runTest จะแตก เป็นไปได้สำหรับการอัปเดตห้องสมุด Coroutine ในอนาคตเพื่อเปลี่ยนพฤติกรรมของห้องสมุดนี้เป็นผล เราจะใช้ความพยายามทุกวิถีทางเพื่อให้แน่ใจว่ามีความมั่นคงด้านพฤติกรรมเช่นกันจนกว่าการพึ่งพา 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() เพื่อยุติการสำรอง coroutines ใด ๆ ในที่สุดคุณสามารถยืนยันได้ว่าเหตุการณ์ทั้งหมดถูกใช้โดยการโทรหา 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() จะถูกเรียกใช้เมื่อการโทร coroutine เสร็จสมบูรณ์
testIn ไม่สามารถทำความสะอาด coroutine โดยอัตโนมัติดังนั้นจึงขึ้นอยู่กับคุณเพื่อให้แน่ใจว่าการไหลของการไหลจะสิ้นสุดลง ใช้ 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 แต่ในตอนท้ายของการโทร 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
อย่างไรก็ตามเหตุการณ์ที่ได้รับสามารถถูกละเว้นได้อย่างชัดเจน
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)
}ความล้มเหลวในการบริโภคข้อผิดพลาดจะส่งผลให้เกิดข้อยกเว้นเหตุการณ์ที่ไม่ได้รับการรับรองเช่นเดียวกับข้างต้น แต่ด้วยข้อยกเว้นที่เพิ่มขึ้นเป็นสาเหตุเพื่อให้มีการใช้งานเต็มรูปแบบ
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 S ที่สร้างขึ้นจากการไหลแล้ว 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())
}
} เพื่อรองรับรหัสฐานด้วยการผสมผสานของ coroutines และรหัสที่ไม่ใช่ coroutines 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 พวกเขาจะโยนเมื่อใช้จากบริบทที่ถูกระงับ
การไหลเป็นแบบอะซิงโครนัสโดยค่าเริ่มต้น การไหลของคุณถูกรวบรวมพร้อมกับกังหันพร้อมกับรหัสทดสอบของคุณ
การจัดการกับความสัมพันธ์แบบอะซิงโครนิกนี้ทำงานในลักษณะเดียวกันกับกังหันเช่นเดียวกับในการผลิตรหัส coroutines: แทนที่จะใช้เครื่องมือเช่น runCurrent() เพื่อ "ผลักดัน" การไหลแบบอะซิงโครนัสไปตาม awaitItem() ของ Turbine awaitComplete() , awaitError() ดึง "พวกเขาไปด้วยที่จอดรถจนกว่าจะมีเหตุการณ์ใหม่พร้อม
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())
} กังหันสามารถตั้งชื่อเพื่อปรับปรุงข้อเสนอแนะข้อผิดพลาด ส่งผ่านใน 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 ของกังหันส่วนใหญ่ถูกนำไปใช้เป็นส่วนขยายใน 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.