التوربينات هي مكتبة اختبار صغيرة 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 " )
} على الرغم من أن واجهة برمجة تطبيقات Turbine الخاصة مستقرة ، إلا أننا مجبرون حاليًا على الاعتماد على واجهة برمجة تطبيقات غير مستقرة من kotlinx.coroutines intifact: UnconfinedTestDispatcher . بدون هذا الاستخدام من التوربينات مع runTest . من الممكن تحديثات مكتبة Coroutine المستقبلية لتغيير سلوك هذه المكتبة نتيجة لذلك. سنبذل قصارى جهدنا لضمان الاستقرار السلوكي أيضًا حتى يتم تثبيت تبعية واجهة برمجة التطبيقات (مشكلة التتبع رقم 132).
Turbine عبارة عن غلاف رفيع فوق Channel مع واجهة برمجة تطبيقات مصممة للاختبار.
يمكنك الاتصال 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)
}سيؤدي الفشل في استهلاك خطأ إلى نفس استثناء الحدث غير المستهلك على النحو الوارد أعلاه ، ولكن مع استثناء إضافة السبب بحيث يتوفر 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 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())
}
} لدعم قواعد الكود مع مزيج من الكوروتين ورمز غير الكوروتين ، تشتمل Turbine المستقلة على واجهات برمجة التطبيقات غير المتوافقة. جميع أساليب 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 ، سوف يرمون عند استخدامه من سياق تعليق.
التدفقات غير متزامنة بشكل افتراضي. يتم جمع التدفق الخاص بك بشكل متزامن عن طريق التوربين إلى جانب رمز الاختبار الخاص بك.
إن التعامل مع هذا التزامن يعمل بنفس الطريقة مع التوربينات كما هو الحال في كود الإنتاج: بدلاً من استخدام أدوات مثل runCurrent() "لدفع" تدفق غير متزامن ، وينتظر Turbine awaitItem() ، 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) {
.. .
} يتم تنفيذ معظم واجهات برمجة تطبيقات التوربينات كملحقات على Channel . عادة ما يكون سطح API الأكثر محدودًا Turbine هو الأفضل ، ولكن هذه الامتدادات متوفرة أيضًا كواجهة برمجة تطبيقات عامة إذا كنت في حاجة إليها.
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.