طريقة اختبار الوحدات أو الوحدات التي تتوافق مع تدفق ما إذا كان الشخص قيد الاختبار يستخدم التدفق كمدخل أو إخراج.
- إذا لاحظ الشخص قيد الاختبار تدفقًا، فيمكنك إنشاء تدفقات داخل للتبعيات المزيفة التي يمكنك التحكم فيها من خلال الاختبارات.
- إذا كشفت الوحدة أو الوحدة عن تدفق، يمكنك قراءة أحدها أو التحقق منه العناصر المتعددة المنبعثة من تدفق في الاختبار.
إنشاء منتج مزيّف
عندما يكون الشخص قيد الاختبار مستهلكًا لتدفق، فإن إحدى الطرق الشائعة لاختباره هي إحلال طريقة تنفيذ مزيفة محل المنتج. على سبيل المثال، إذا كانت فئة ترصد المستودع الذي يأخذ البيانات من مصدرين للبيانات في الإنتاج:
لجعل الاختبار حتميًا، يمكنك استبدال المستودع تبعيات ذات مستودع مزيف ينبعث دائمًا نفس البيانات المزيفة:
لإصدار سلسلة من القيم المحدّدة مسبقًا في التدفق، استخدِم أداة إنشاء flow
:
class MyFakeRepository : MyRepository {
fun observeCount() = flow {
emit(ITEM_1)
}
}
أثناء الاختبار، تم حقن هذا المستودع المزيف ليحل محل المستودع الحقيقي التنفيذ:
@Test
fun myTest() {
// Given a class with fake dependencies:
val sut = MyUnitUnderTest(MyFakeRepository())
// Trigger and verify
...
}
والآن بعد أن أصبح بإمكانك التحكم في مخرجات الموضوع قيد الاختبار، يمكنك التحقق من أنه يعمل بشكل صحيح عن طريق التحقق من النتائج.
التأكيد على انبعاثات التدفق في اختبار
إذا كان الشخص قيد الاختبار يعرض تدفقًا، يحتاج الاختبار إلى تأكيد على عناصر مصدر البيانات.
لنفترض أن مستودع المثال السابق يعرض تدفقًا:
وفي بعض الاختبارات، ما عليك سوى التحقق من أول انبعاثات أو حد عدد العناصر القادمة من التدفق.
يمكنك استهلاك أول انبعاثات من التدفق من خلال طلب الرقم first()
. هذا النمط
تنتظر الدالة حتى يتم استلام العنصر الأول ثم ترسل رسالة الإلغاء
إشارة للمنتج.
@Test
fun myRepositoryTest() = runTest {
// Given a repository that combines values from two data sources:
val repository = MyRepository(fakeSource1, fakeSource2)
// When the repository emits a value
val firstItem = repository.counter.first() // Returns the first item in the flow
// Then check it's the expected item
assertEquals(ITEM_1, firstItem)
}
إذا احتاج الاختبار إلى التحقّق من قيم متعدّدة، سيؤدي طلب toList()
إلى بدء التدفق.
الانتظار حتى ينبعث المصدر جميع قيمه، ثم يتم إرجاع هذه القيم
الحالية. ولا تعمل هذه الطريقة إلا مع مصادر البيانات المحدودة.
@Test
fun myRepositoryTest() = runTest {
// Given a repository with a fake data source that emits ALL_MESSAGES
val messages = repository.observeChatMessages().toList()
// When all messages are emitted then they should be ALL_MESSAGES
assertEquals(ALL_MESSAGES, messages)
}
بالنسبة إلى مصادر البيانات التي تتطلّب مجموعة أكثر تعقيدًا من السلع أو لا تعرِضها
لعدد محدود من العناصر، يمكنك استخدام واجهة برمجة تطبيقات Flow
لاختيار وتحويل
عناصر. في ما يلي بعض الأمثلة:
// Take the second item
outputFlow.drop(1).first()
// Take the first 5 items
outputFlow.take(5).toList()
// Takes the first item verifying that the flow is closed after that
outputFlow.single()
// Finite data streams
// Verify that the flow emits exactly N elements (optional predicate)
outputFlow.count()
outputFlow.count(predicate)
جمع متواصل أثناء الاختبار
استخدامات عملية جمع التدفق باستخدام toList()
كما هو موضح في المثال السابق
collect()
داخليًا ويتم تعليقه إلى أن تصبح قائمة النتائج بالكامل جاهزة
عاد.
لتداخل الإجراءات التي تتسبب في انبعاث قيم وتأكيدات من التدفق على القيم المنبعثة، يمكنك جمع القيم باستمرار من التدفق أثناء الاختبار.
على سبيل المثال، شارِك في اختبار الفئة Repository
التالية
مرافق تنفيذ مصدر بيانات مزيّف يستخدم طريقة emit
إنتاج القيم ديناميكيًا أثناء الاختبار:
class Repository(private val dataSource: DataSource) {
fun scores(): Flow<Int> {
return dataSource.counts().map { it * 10 }
}
}
class FakeDataSource : DataSource {
private val flow = MutableSharedFlow<Int>()
suspend fun emit(value: Int) = flow.emit(value)
override fun counts(): Flow<Int> = flow
}
وعند استخدام هذا المنتج المزيّف في أحد الاختبارات، يمكنك إنشاء مجموعة من الكوروتينات
تتلقّى القيم باستمرار من Repository
. في هذا المثال،
وجمعها في قائمة ثم التأكيد على محتوياتها:
@Test
fun continuouslyCollect() = runTest {
val dataSource = FakeDataSource()
val repository = Repository(dataSource)
val values = mutableListOf<Int>()
backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
repository.scores().toList(values)
}
dataSource.emit(1)
assertEquals(10, values[0]) // Assert on the list contents
dataSource.emit(2)
dataSource.emit(3)
assertEquals(30, values[2])
assertEquals(3, values.size) // Assert the number of items collected
}
بما أنّ التدفق الذي يعرضه Repository
هنا لا يكتمل أبدًا، فإنّ toList
التي تجمعها لا تعود أبدًا. جارٍ بدء جمع الكوروتين في
TestScope.backgroundScope
ويضمن إلغاء الكوروتين قبل نهاية الاختبار. وبخلاف ذلك،
سيواصل "runTest
" الانتظار حتى اكتمال الاختبار، ما يؤدي إلى إيقاف الاختبار.
يستجيب وتفشل في النهاية.
لاحظ كيف
UnconfinedTestDispatcher
يُستخدم لجمع الكوروتين هنا. يضمن ذلك أن جمع
يتم إطلاق الكوروتين بحرص وهو جاهز لتلقّي القيم بعد launch
.
وإرجاعه.
استخدام التوربينات
تعرض الجهة الخارجية Turbine واجهة برمجة تطبيقات مناسبة لإنشاء الكوروتينات، وكذلك كميزات ملائمة أخرى لاختبار التدفقات:
@Test
fun usingTurbine() = runTest {
val dataSource = FakeDataSource()
val repository = Repository(dataSource)
repository.scores().test {
// Make calls that will trigger value changes only within test{}
dataSource.emit(1)
assertEquals(10, awaitItem())
dataSource.emit(2)
awaitItem() // Ignore items if needed, can also use skip(n)
dataSource.emit(3)
assertEquals(30, awaitItem())
}
}
يمكنك الاطّلاع على وثائق المكتبة المزيد من التفاصيل.
اختبار StateFlows
StateFlow
يمكن ملاحظته.
صاحب البيانات، والتي يمكن جمعها لمراقبة القيم التي يحتفظ بها بمرور الوقت
تدفق. لاحظ أنه يتم الخلط بين تدفق القيم، وهذا يعني أنه إذا
يتم ضبط القيم في StateFlow
بسرعة، فلن يتم تطبيق القيم التي تجمعها StateFlow
مضمونًا تلقي جميع القيم المتوسطة، الأحدث فقط.
في الاختبارات، إذا كنت تضع الخلط في الاعتبار، يمكنك جمع قيم StateFlow
كما يمكنك جمع أي تدفقات أخرى، بما في ذلك Turbine. جارٍ محاولة جمع
وتأكيدها على جميع القيم المتوسطة أمرًا مرغوبًا فيه في بعض سيناريوهات الاختبار.
مع ذلك، ننصح بشكل عام بالتعامل مع StateFlow
كصاحب بيانات.
تأكيده على خاصية value
بدلاً من ذلك. بهذه الطريقة، تتحقق الاختبارات من صحة
حالة الجسم في نقطة زمنية معينة، ولا تعتمد على ما إذا كان
الخلاف.
على سبيل المثال، لنأخذ نموذج ViewModel هذا الذي يجمع القيم من Repository
يعرضها على واجهة المستخدم في StateFlow
:
class MyViewModel(private val myRepository: MyRepository) : ViewModel() {
private val _score = MutableStateFlow(0)
val score: StateFlow<Int> = _score.asStateFlow()
fun initialize() {
viewModelScope.launch {
myRepository.scores().collect { score ->
_score.value = score
}
}
}
}
قد تظهر عملية التنفيذ الزائفة لـ Repository
على النحو التالي:
class FakeRepository : MyRepository {
private val flow = MutableSharedFlow<Int>()
suspend fun emit(value: Int) = flow.emit(value)
override fun scores(): Flow<Int> = flow
}
وعند اختبار ViewModel باستخدام هذا النموذج الزائف، يمكنك إرسال قيم من النموذج المزيف إلى
تشغيل التحديثات في StateFlow
's في ViewModel، ثم التأكيد على
value
:
@Test
fun testHotFakeRepository() = runTest {
val fakeRepository = FakeRepository()
val viewModel = MyViewModel(fakeRepository)
assertEquals(0, viewModel.score.value) // Assert on the initial value
// Start collecting values from the Repository
viewModel.initialize()
// Then we can send in values one by one, which the ViewModel will collect
fakeRepository.emit(1)
assertEquals(1, viewModel.score.value)
fakeRepository.emit(2)
fakeRepository.emit(3)
assertEquals(3, viewModel.score.value) // Assert on the latest value
}
العمل باستخدام StateFlows الذي تم إنشاؤه من قِبل StateIn
في القسم السابق، يستخدم ViewModel MutableStateFlow
لتخزين
أحدث قيمة تنتج عن تدفّق من Repository
. هذا نمط شائع،
عادةً ما يتم تنفيذها بطريقة أبسط باستخدام
stateIn
الذي يحوّل التدفق البارد إلى StateFlow
ساخن:
class MyViewModelWithStateIn(myRepository: MyRepository) : ViewModel() {
val score: StateFlow<Int> = myRepository.scores()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000L), 0)
}
يتضمّن عامل التشغيل stateIn
مَعلمة SharingStarted
التي تحدّد الوقت الذي
يصبح نشطًا ويبدأ في استهلاك التدفق الأساسي. خيارات مثل
يتم استخدام SharingStarted.Lazily
وSharingStarted.WhileSubsribed
بشكل متكرّر.
في ViewModels.
حتى إذا كنت تؤكد على value
من StateFlow
في اختبارك، عليك
تحتاج إليها لإنشاء جامع. يمكن أن يكون هذا جامعًا فارغًا:
@Test
fun testLazilySharingViewModel() = runTest {
val fakeRepository = HotFakeRepository()
val viewModel = MyViewModelWithStateIn(fakeRepository)
// Create an empty collector for the StateFlow
backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
viewModel.score.collect()
}
assertEquals(0, viewModel.score.value) // Can assert initial value
// Trigger-assert like before
fakeRepository.emit(1)
assertEquals(1, viewModel.score.value)
fakeRepository.emit(2)
fakeRepository.emit(3)
assertEquals(3, viewModel.score.value)
}
مصادر إضافية
- اختبار الكوروتينات بلغة Kotlin على أجهزة Android
- مسارات Kotlin على Android
StateFlow
وSharedFlow
- مراجع إضافية حول الكوروتينات في لغة Kotlin وتدفق البيانات