تعتمد طريقة اختبار الوحدات أو الوحدات النمطية التي تتواصل مع المسار على ما إذا كان الموضوع الذي يتم اختباره يستخدم المسار كمدخل أو مخرج.
- إذا لاحظ الموضوع الخاضع للاختبار تدفّقًا، يمكنك إنشاء تدفّقات ضمن تبعيات زائفة يمكنك التحكّم فيها من الاختبارات.
- إذا كانت الوحدة أو الوحدة النمطية تعرض عملية معالجة، يمكنك قراءة عنصر واحد أو عناصر متعددة تنشرها عملية المعالجة في الاختبار والتحقّق منها.
إنشاء منتج مزيّف
عندما يكون الموضوع الذي يتم اختباره مستهلكًا لأحد المسارات، من الطرق الشائعة لاختباره استبدال المُنشئ بتنفيذ مزيّف. على سبيل المثال، في حال توفّر ملف برمجي يرصد مستودعًا يستخرج البيانات من مصدرَين للبيانات في مرحلة النشر:
لجعل الاختبار حتميًا، يمكنك استبدال المستودع ومكوّناته التابعة بمستودع مزيّف يُصدر دائمًا البيانات المزيّفة نفسها:
لعرض سلسلة قيم محدّدة مسبقًا في مسار، استخدِم أداة إنشاء 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
call التي تجمعها. يؤدّي بدء دالة جمع البيانات في TestScope.backgroundScope
إلى إلغاء دالة جمع البيانات قبل نهاية الاختبار. بخلاف ذلك، سيستمر runTest
في الانتظار إلى أن يكتمل الاختبار، ما يؤدي إلى إيقاف الاختبار عن الردّ وبالتالي تعذُّر إكماله.
لاحظ كيفية استخدام
UnconfinedTestDispatcher
لحلقة الاستدعاء المتعدّد المجمّعة هنا. يضمن ذلك بدء coroutine لجمع
القيم بشكلٍ فوري وجاهزيته لتلقّي القيم بعد عرض launch
.
استخدام Turbine
توفّر مكتبة 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
من 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.WhileSubscribed
بشكل متكرّر
في نماذج العرض.
حتى إذا كنت تُثبت 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 المتعدّدة المهام ووظائف Flow