Turbin adalah perpustakaan pengujian kecil untuk Flow Kotlinx.coroutine.
flowOf( " one " , " two " ).test {
assertEquals( " one " , awaitItem())
assertEquals( " two " , awaitItem())
awaitComplete()
}Turbin adalah perangkat mekanik putar yang mengekstraksi energi dari aliran fluida dan mengubahnya menjadi pekerjaan yang bermanfaat.
- 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 " )
} Sementara API Turbine sendiri stabil, kami saat ini dipaksa untuk bergantung pada API yang tidak stabil dari Kotlinx.Coroutine Test Artifact: UnconfinedTestDispatcher . Tanpa penggunaan turbin dengan runTest ini akan pecah. Adalah mungkin bagi pembaruan Perpustakaan Coroutine di masa depan untuk mengubah perilaku perpustakaan ini sebagai hasilnya. Kami akan melakukan segala upaya untuk memastikan stabilitas perilaku juga sampai ketergantungan API ini distabilkan (masalah pelacakan #132).
Turbine adalah pembungkus tipis di atas Channel dengan API yang dirancang untuk pengujian.
Anda dapat menelepon awaitItem() untuk menangguhkan dan menunggu item dikirim ke Turbine :
assertEquals( " one " , turbine.awaitItem()) ... awaitComplete() untuk menangguhkan sampai Turbine selesai tanpa pengecualian:
turbine.awaitComplete() ... atau awaitError() untuk menangguhkan sampai Turbine selesai dengan yang Throwable .
assertEquals( " broken! " , turbine.awaitError().message) Jika await* dipanggil dan tidak ada yang terjadi, Turbine akan waktu habis dan gagal alih -alih menggantung.
Ketika Anda selesai dengan Turbine , Anda dapat membersihkan dengan menelepon cancel() untuk mengakhiri setiap backing coroutine. Akhirnya, Anda dapat menegaskan bahwa semua acara dikonsumsi dengan memanggil ensureAllEventsConsumed() .
Cara paling sederhana untuk membuat dan menjalankan Turbine adalah menghasilkan satu dari Flow . Untuk menguji satu Flow , hubungi ekstensi test :
someFlow.test {
// Validation code here!
} test meluncurkan coroutine baru, memanggil someFlow.collect , dan memasukkan hasilnya menjadi Turbine . Kemudian memanggil blok validasi, lewat antarmuka read-only ReceiveTurbine sebagai penerima:
flowOf( " one " ).test {
assertEquals( " one " , awaitItem())
awaitComplete()
} Ketika blok validasi selesai, test membatalkan coroutine dan panggilan ensureAllEventsConsumed() .
Untuk menguji beberapa aliran, tetapkan setiap Turbine ke val terpisah dengan memanggil testIn sebagai gantinya:
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()
}
} Seperti test , testIn menghasilkan ReceiveTurbine . ensureAllEventsConsumed() akan dipanggil ketika panggilan coroutine selesai.
testIn tidak dapat secara otomatis membersihkan coroutine -nya, jadi terserah Anda untuk memastikan bahwa aliran yang berjalan berakhir. Gunakan backgroundScope runTest , dan itu akan mengurus ini secara otomatis. Jika tidak, pastikan untuk memanggil salah satu metode berikut sebelum akhir ruang lingkup Anda:
cancel()awaitComplete()awaitError()Kalau tidak, tes Anda akan menggantung.
Gagal mengkonsumsi semua peristiwa sebelum akhir blok validasi Turbine berbasis aliran akan gagal dalam tes Anda:
flowOf( " one " , " two " ).test {
assertEquals( " one " , awaitItem())
} Exception in thread "main" AssertionError:
Unconsumed events found:
- Item(two)
- Complete
Hal yang sama berlaku untuk testIn , tetapi di akhir panggilan 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
Namun, peristiwa yang diterima dapat diabaikan secara eksplisit.
flowOf( " one " , " two " ).test {
assertEquals( " one " , awaitItem())
cancelAndIgnoreRemainingEvents()
}Selain itu, kita dapat menerima item yang dipancarkan terbaru dan mengabaikan yang sebelumnya.
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()
} Peristiwa pemutusan aliran (pengecualian dan penyelesaian) diekspos sebagai peristiwa yang harus dikonsumsi untuk validasi. Jadi, misalnya, melempar RuntimeException di dalam flow Anda tidak akan melempar pengecualian dalam tes Anda. Itu malah akan menghasilkan peristiwa kesalahan turbin:
flow { throw RuntimeException ( " broken! " ) }.test {
assertEquals( " broken! " , awaitError().message)
}Kegagalan untuk mengkonsumsi kesalahan akan menghasilkan pengecualian peristiwa yang tidak dikonsumsi yang sama seperti di atas, tetapi dengan pengecualian ditambahkan sebagai penyebab sehingga stacktrace penuh tersedia.
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
Selain ReceiveTurbine yang dibuat dari aliran, Turbine mandiri dapat digunakan untuk berkomunikasi dengan kode uji di luar aliran. Gunakan di mana -mana, dan Anda mungkin tidak perlu runCurrent() lagi. Berikut adalah contoh cara menggunakan Turbine() secara palsu:
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())
}
} Untuk mendukung basis kode dengan campuran kode coroutine dan non-koroutin, Turbine mandiri mencakup API yang tidak tertutupi. Semua metode await memiliki metode take yang setara yang tidak menanggapi:
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()) Gunakan takeItem() dan teman -teman, dan Turbine berperilaku seperti antrian sederhana; Gunakan awaitItem() dan teman -teman, dan itu adalah Turbine .
Metode-metode ini hanya boleh digunakan dari konteks non-penangguhan. Pada platform JVM, mereka akan melempar ketika digunakan dari konteks yang menangguhkan.
Aliran tidak sinkron secara default. Aliran Anda dikumpulkan secara bersamaan dengan turbin di samping kode pengujian Anda.
Menangani asinkronisitas ini bekerja dengan cara yang sama dengan turbin seperti halnya dalam produksi kode coroutines: alih -alih menggunakan alat seperti runCurrent() untuk "mendorong" aliran asinkron di sepanjang, Turbine awaitItem() , awaitComplete() , dan awaitError() " Tarik "mereka bersama dengan parkir sampai acara baru siap.
channelFlow {
withContext( IO ) {
Thread .sleep( 100 )
send( " item " )
}
}.test {
assertEquals( " item " , awaitItem())
awaitComplete()
} Kode validasi Anda dapat berjalan secara bersamaan dengan aliran yang sedang diuji, tetapi turbin meletakkannya di kursi pengemudi sebanyak mungkin: test akan berakhir ketika blok validasi Anda selesai dieksekusi, secara implisit membatalkan aliran yang diuji.
channelFlow {
withContext( IO ) {
repeat( 10 ) {
Thread .sleep( 200 )
send( " item $it " )
}
}
}.test {
assertEquals( " item 0 " , awaitItem())
assertEquals( " item 1 " , awaitItem())
assertEquals( " item 2 " , awaitItem())
}Aliran juga dapat dibatalkan secara eksplisit di titik mana pun.
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())
} Turbin dapat dinamai untuk meningkatkan umpan balik kesalahan. Lulus dalam name untuk test , testIn , atau Turbine() , dan itu akan dimasukkan dalam kesalahan apa pun yang dilemparkan:
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)
...
Aliran bersama sensitif terhadap urutan eksekusi. Panggilan emit sebelum memanggil collect akan menjatuhkan nilai yang dipancarkan:
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 Turbine dan metode testIn menjamin bahwa aliran yang diuji akan berjalan hingga titik suspensi pertama sebelum melanjutkan. Jadi test Memanggil pada Aliran Bersama Sebelum Memancarkan Tidak Akan Menjatuhkan:
val mutableSharedFlow = MutableSharedFlow < Int >(replay = 0 )
mutableSharedFlow.test {
mutableSharedFlow.emit( 1 )
assertEquals(awaitItem(), 1 )
}Jika kode Anda dikumpulkan pada aliran bersama, pastikan itu segera memiliki pengalaman yang indah.
Jenis aliran bersama yang saat ini disediakan Kotlin adalah:
MutableStateFlowStateFlowMutableSharedFlowSharedFlow Turbin menerapkan batas waktu setiap kali menunggu acara. Ini adalah batas waktu waktu dinding waktu yang mengabaikan waktu jam virtual runTest .
Panjang batas waktu default adalah tiga detik. Ini bisa ditimpa dengan melewati durasi batas waktu untuk test :
flowOf( " one " , " two " ).test(timeout = 10 .milliseconds) {
.. .
}Timeout ini akan digunakan untuk semua panggilan terkait turbin di dalam blok validasi.
Anda juga dapat mengganti batas waktu untuk turbin yang dibuat dengan testIn dan Turbine() :
val standalone = Turbine < String >(timeout = 10 .milliseconds)
val flow = flowOf( " one " ).testIn(
scope = backgroundScope,
timeout = 10 .milliseconds,
) Timeout ini mengesampingkan hanya berlaku untuk Turbine tempat mereka diterapkan.
Akhirnya, Anda juga dapat mengubah batas waktu untuk seluruh blok kode menggunakan withTurbineTimeout :
withTurbineTimeout( 10 .milliseconds) {
.. .
} Sebagian besar API Turbine diimplementasikan sebagai ekstensi pada Channel . Permukaan API Turbine yang lebih terbatas biasanya lebih disukai, tetapi ekstensi ini juga tersedia sebagai API publik jika Anda membutuhkannya.
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.