يتطلب رمز اختبار الوحدة الذي يستخدم coroutine بعض الاهتمام الإضافي، حيث يمكن أن يكون تنفيذه غير متزامن ويحدث عبر سلاسل محادثات متعددة. يتناول هذا الدليل كيفية اختبار دوال التعليق، وتركيبات الاختبار التي تحتاج إلى أن تكون على دراية بها، وكيفية جعل التعليمة البرمجية التي تستخدم الكوروتينات قابلة للاختبار.
تشكِّل واجهات برمجة التطبيقات المستخدَمة في هذا الدليل جزءًا من مكتبة kotlinx.coroutines.test. تأكَّد من إضافة العنصر كتبعية تجريبية لمشروعك حتى تتمكّن من الوصول إلى واجهات برمجة التطبيقات هذه.
dependencies {
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version"
}
استدعاء وظائف التعليق في الاختبارات
لاستدعاء وظائف التعليق في الاختبارات، يجب أن تكون في الكوروتين. نظرًا لأن وظائف اختبار JUnit نفسها لا تقوم بتعليق الدوال، فأنت بحاجة إلى استدعاء أداة إنشاء الكوروتين داخل اختباراتك لبدء تشغيل كوروتين جديد.
runTest
هي أداة إنشاء الكوروتين مُصمَّمة للاختبار. استخدم هذا لإكمال أي اختبارات تحتوي على الكوروتينات. تجدر الإشارة إلى أنَّ مادة الكوروتين يمكن أن تبدأ ليس فقط في جسم الاختبار بشكل مباشر، ولكن أيضًا عن طريق العناصر التي يتم استخدامها في الاختبار.
suspend fun fetchData(): String { delay(1000L) return "Hello world" } @Test fun dataShouldBeHelloWorld() = runTest { val data = fetchData() assertEquals("Hello world", data) }
وبشكل عام، يجب أن يكون لديك استدعاء واحد لـ runTest
في كل اختبار، ويُنصح باستخدام نص إعلان.
يساعد التفاف رمز الاختبار في runTest
على اختبار وظائف التعليق الأساسية، وسيتم تلقائيًا تخطي أي تأخيرات في الكوروتينات، ما يجعل الاختبار أعلاه أسرع بكثير من ثانية واحدة.
ومع ذلك، هناك اعتبارات إضافية يجب مراعاتها، بناءً على ما يحدث في الرمز البرمجي الذي يخضع له الاختبار:
- عندما يُنشئ رمزك الكوروتينات الجديدة غير الكوروتين الاختباري ذي المستوى الأعلى الذي ينشئه
runTest
، ستحتاج إلى التحكّم في كيفية جدولة الكوروتينات الجديدة هذه عن طريق اختيارTestDispatcher
المناسب. - إذا كان الرمز الخاص بك ينقل عملية تنفيذ الكوروتين إلى مُرسِلين آخرين (باستخدام
withContext
مثلاً)، سيستمر عملrunTest
بشكلٍ عام، ولكن لن يتم تخطّي حالات التأخير، وسيكون من الصعب توقُّع الاختبارات عندما يتم تشغيل الرمز في سلاسل محادثات متعددة. ولهذه الأسباب، وفي الاختبارات، يجب إدخال مرسِلي الاختبار لاستبدال المُرسِلين الحقيقيين.
مرسِلو الاختبار
TestDispatchers
هي عمليات تنفيذ CoroutineDispatcher
لأغراض الاختبار. سيكون عليك استخدام TestDispatchers
في حال إنشاء الكوروتينات الجديدة أثناء الاختبار لتسهيل تنفيذ الكوروتينات الجديدة.
يتوفّر نوعان من عمليات تنفيذ الترميز TestDispatcher
: StandardTestDispatcher
وUnconfinedTestDispatcher
، واللذَين ينفِّذان جدولة مختلفة لعناصر الكوروتين التي تم بدؤها حديثًا. يستخدم كلاهما TestCoroutineScheduler
للتحكّم في الوقت الافتراضي وإدارة تشغيل الكوروتينات داخل الاختبار.
يجب استخدام مثيل واحد فقط لأداة جدولة المهام في الاختبار، وتتم مشاركته بين جميع TestDispatchers
. راجع Injecting TestDispatchers لمعرفة معلومات عن مشاركة أدوات جدولة المهام.
لبدء اختبار الكوروتين ذي المستوى الأعلى، ينشئ runTest
TestScope
، وهو تنفيذ CoroutineScope
ستستخدم دائمًا TestDispatcher
. إذا لم يتم تحديد ذلك، سينشئ TestScope
StandardTestDispatcher
تلقائيًا، ويستخدم ذلك لتشغيل الكوروتين من المستوى الأعلى.
يتتبّع runTest
الكوروتينات المدرجة في قائمة الانتظار على نظام الجدولة الذي يستخدمه مرسل TestScope
، ولن يعود ما دام هناك عمل في انتظار المراجعة على نظام الجدولة هذا.
شركة StandardTestDispatcher
عند بدء تشغيل الكوروتينات الجديدة على StandardTestDispatcher
، يتم وضعها في قائمة الانتظار على نظام جدولة المهام الأساسي، ليتم تشغيلها عندما تكون سلسلة المحادثات التجريبية مجانية. للسماح بتشغيل هذه الكوروتينات الجديدة، عليك إرجاع سلسلة الاختبار (إخلاء مساحة كي تتمكن من استخدام الكوروتينات الأخرى). يمنحك سلوك وضع الانتظار هذا تحكمًا دقيقًا في كيفية تشغيل الكوروتينات الجديدة أثناء الاختبار، ويشبه جدولة الكوروتينات في التعليمات البرمجية للإنتاج.
إذا لم يتم عرض سلسلة الاختبار مطلقًا أثناء تنفيذ الكوروتين الاختباري العالي المستوى، لن يتم تشغيل أي كوروتيين جديد إلا بعد انتهاء اختبار الكوروتين (ولكن قبل عرض runTest
):
@Test fun standardTest() = runTest { val userRepo = UserRepository() launch { userRepo.register("Alice") } launch { userRepo.register("Bob") } assertEquals(listOf("Alice", "Bob"), userRepo.getAllUsers()) // ❌ Fails }
وهناك عدة طرق لإخراج اختبار الكوروتين من أجل السماح بتشغيل الكوروتينات الموضوعة في قائمة الانتظار. تتيح كل هذه الطلبات استخدام الكوروتينات الأخرى في سلسلة الاختبار قبل عرضها:
advanceUntilIdle
: لتشغيل جميع أنواع الكوروتين الأخرى على نظام جدولة المهام إلى أن ينتهي كل شيء في قائمة الانتظار. يُعد هذا اختيارًا افتراضيًا جيدًا للسماح بتشغيل جميع أنواع الكوروتين التي تنتظر المراجعة، ويعمل في معظم سيناريوهات الاختبار.advanceTimeBy
: يؤدي هذا الخيار إلى زيادة الوقت الافتراضي بمقدار الحدّ المحدّد وتشغيل أي عناصر كوروتيين تمت جدولتها للتشغيل قبل تلك النقطة في الوقت الافتراضي.runCurrent
: لتشغيل عناصر الكوروتين التي تمت جدولتها في الوقت الافتراضي الحالي
لإصلاح الاختبار السابق، يمكن استخدام advanceUntilIdle
للسماح للكوروتيين المعلّقَين بأداء عملهما قبل المتابعة إلى خطوة التأكيد:
@Test fun standardTest() = runTest { val userRepo = UserRepository() launch { userRepo.register("Alice") } launch { userRepo.register("Bob") } advanceUntilIdle() // Yields to perform the registrations assertEquals(listOf("Alice", "Bob"), userRepo.getAllUsers()) // ✅ Passes }
اختبار UnconfinedTestDispatcher
عند بدء تشغيل الكوروتينات الجديدة على UnconfinedTestDispatcher
، يبدأون بفارغ الصبر في سلسلة المحادثات الحالية. هذا يعني أنه سيبدأ تشغيلها على الفور، دون انتظار عودة أداة إنشاء الكوروتين الخاصة بها. في كثير من الحالات، ينتج عن سلوك الإرسال هذا رمز اختبار أبسط، حيث لا تحتاج إلى عرض سلسلة الاختبار يدويًا للسماح بتشغيل الكوروتينات الجديدة.
ومع ذلك، يختلف هذا السلوك عما ستلاحظه في مرحلة الإنتاج لدى المستخدمين غير التابعين للاختبار. إذا كان الاختبار يركز على التزامن، يُفضَّل استخدام StandardTestDispatcher
بدلاً من ذلك.
لاستخدام أداة الإرسال هذه للكوروتين الاختباري من المستوى الأعلى في runTest
بدلاً من المرسِل التلقائي، يمكنك إنشاء مثيل وتمريره كمَعلمة. سيؤدي هذا الإجراء إلى تنفيذ عناصر الكوروتين الجديدة التي تم إنشاؤها داخل runTest
في الحال، لأنّها تكتسب المرسِل من TestScope
.
@Test fun unconfinedTest() = runTest(UnconfinedTestDispatcher()) { val userRepo = UserRepository() launch { userRepo.register("Alice") } launch { userRepo.register("Bob") } assertEquals(listOf("Alice", "Bob"), userRepo.getAllUsers()) // ✅ Passes }
في هذا المثال، ستبدأ عمليات الإطلاق في الكوروتينات الجديدة الخاصة بهم بلهفة على UnconfinedTestDispatcher
، ما يعني أنّ كل مكالمة للبدء لن تعود إلا بعد اكتمال التسجيل.
تذكَّر أنّ UnconfinedTestDispatcher
يبدأ تشغيل الكوروتينات الجديدة بلهفة، ولكن هذا لا يعني أنه سيُجريها حتى اكتمالها بلهفة أيضًا. في حال تعليق الكوروتين الجديد، سيتم استئناف تنفيذ الكوروتينات الأخرى.
على سبيل المثال، سيسجّل الكوروتين الجديد الذي تم إطلاقه ضمن هذا الاختبار نبيلة، ولكن يتم تعليقه بعد ذلك عند استدعاء delay
. ويسمح هذا لتطبيق الكوروتين ذي المستوى الأعلى بمواصلة التأكيد، وسيتعذّر الاختبار لأنّه لم يتم تسجيل يوسف بعد:
@Test fun yieldingTest() = runTest(UnconfinedTestDispatcher()) { val userRepo = UserRepository() launch { userRepo.register("Alice") delay(10L) userRepo.register("Bob") } assertEquals(listOf("Alice", "Bob"), userRepo.getAllUsers()) // ❌ Fails }
حقن مرسلي الاختبارات
في حال كان الرمز قيد الاختبار، قد يتم استخدام المُرسِلين لتبديل سلاسل المحادثات (باستخدام withContext
) أو بدء الكوروتينات الجديدة. عندما يتم تنفيذ الرمز على سلاسل تعليمات متعددة بالتوازي، يمكن أن تصبح الاختبارات غير مستقرة. قد يكون من الصعب تنفيذ التأكيدات في الوقت الصحيح أو انتظار اكتمال المهام إذا كانت تعمل على سلاسل محادثات في الخلفية لا يمكنك التحكم فيها.
في الاختبارات، يمكنك استبدال المُرسِلين بمثيلات TestDispatchers
. وهذا له فوائد عديدة:
- سيتم تشغيل الرمز على سلسلة الاختبار الفردية، ما يجعل الاختبارات أكثر حتمية.
- يمكنك التحكّم في كيفية جدولة عناصر الكوروتين الجديدة وتنفيذها.
- يستخدم TestDispatchers برنامج جدولة للوقت الافتراضي، والذي يتخطى التأخيرات تلقائيًا وتسمح لك بتقديم الوقت يدويًا
يُسهِّل استخدام إدخال الاعتمادية لتزويد المرسلين إلى الصفوف الدراسية عليك استبدال المُرسِلين الحقيقيين في الاختبارات. سنطبّق في هذه الأمثلة نوع CoroutineDispatcher
، ولكن يمكنك أيضًا إدخال النوع الأوسع نطاقًا CoroutineContext
، ما يتيح مرونة أكبر أثناء إجراء الاختبارات.
بالنسبة إلى الصفوف التي تبدأ باستخدام الكوروتينات، يمكنك أيضًا إدخال CoroutineScope
بدلاً من أداة الإرسال، كما هو موضّح بالتفصيل في القسم إدخال نطاق.
ستنشئ TestDispatchers
تلقائيًا أداة جدولة جديدة عند إنشاء مثيل لها. في runTest
، يمكنك الوصول إلى السمة testScheduler
من TestScope
وتمريرها إلى أي TestDispatchers
تم إنشاؤها حديثًا. سيؤدي ذلك إلى مشاركة فهمهم للوقت الافتراضي، وسيتم من خلال طرق مثل advanceUntilIdle
تشغيل الكوروتينات على جميع أجهزة إرسال الاختبار حتى اكتمالها.
في المثال التالي، يمكنك الاطّلاع على فئة Repository
تنشئ الكوروتين الجديد باستخدام مرسِل IO
في طريقة initialize
وتنقل المتصل إلى مرسِل IO
من خلال طريقة fetchData
:
// Example class demonstrating dispatcher use cases class Repository(private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) { private val scope = CoroutineScope(ioDispatcher) val initialized = AtomicBoolean(false) // A function that starts a new coroutine on the IO dispatcher fun initialize() { scope.launch { initialized.set(true) } } // A suspending function that switches to the IO dispatcher suspend fun fetchData(): String = withContext(ioDispatcher) { require(initialized.get()) { "Repository should be initialized first" } delay(500L) "Hello world" } }
في الاختبارات، يمكنك إدخال عملية تنفيذ TestDispatcher
لاستبدال مُرسِل IO
.
في المثال أدناه، تم إدخال StandardTestDispatcher
في المستودع ونستخدم advanceUntilIdle
للتأكد من اكتمال عملية الكوروتين الجديد التي بدأت في initialize
قبل المتابعة.
سيستفيد "fetchData
" أيضًا من التشغيل على "TestDispatcher
"، لأنّه سيتم تنفيذه في سلسلة محادثات الاختبار وتخطّي مدة التأخير التي يتضمّنها أثناء الاختبار.
class RepositoryTest { @Test fun repoInitWorksAndDataIsHelloWorld() = runTest { val dispatcher = StandardTestDispatcher(testScheduler) val repository = Repository(dispatcher) repository.initialize() advanceUntilIdle() // Runs the new coroutine assertEquals(true, repository.initialized.get()) val data = repository.fetchData() // No thread switch, delay is skipped assertEquals("Hello world", data) } }
يمكن يدويًا تقديم برامج الكوروتين الجديدة التي تم تشغيلها على TestDispatcher
كما هو موضّح أعلاه باستخدام initialize
. مع ذلك، يُرجى العلم أنّ هذا الإجراء قد لا يكون ممكنًا أو غير مرغوب فيه في رمز الإنتاج. وبدلاً من ذلك، يجب إعادة تصميم هذه الطريقة بحيث تكون مُعلّقة (للتنفيذ التسلسلي)، أو لعرض قيمة Deferred
(للتنفيذ المتزامن).
على سبيل المثال، يمكنك استخدام async
لبدء كوروتيين جديد وإنشاء Deferred
:
class BetterRepository(private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) { private val scope = CoroutineScope(ioDispatcher) fun initialize() = scope.async { // ... } }
يتيح لك ذلك await
إكمال هذا الرمز بأمان في كلٍّ من الاختبارات ورمز الإنتاج:
@Test fun repoInitWorks() = runTest { val dispatcher = StandardTestDispatcher(testScheduler) val repository = BetterRepository(dispatcher) repository.initialize().await() // Suspends until the new coroutine is done assertEquals(true, repository.initialized.get()) // ... }
سينتظر "runTest
" اكتمال وحدات الكوروتين المعلّقة قبل العودة إذا كانت أجهزة الكوروتين على جهاز TestDispatcher
يتشارك معها نظام جدولة. وسينتظر أيضًا الكوروتينات الصغيرة من الكوروتينات التجريبية ذات المستوى الأعلى، حتى إذا كانت ضمن مرسِلين آخرين (تصل إلى مهلة تحدِّدها المعلَمة dispatchTimeoutMs
، والتي تكون 60 ثانية تلقائيًا).
تحديد المرسل الرئيسي
في اختبارات الوحدات المحلية، لن يكون مُرسِل Main
الذي يلفّ سلسلة محادثات واجهة مستخدم Android متاحًا، لأنّه يتم تنفيذ هذه الاختبارات على جهاز JVM محلي وليس على جهاز Android. إذا كانت التعليمة البرمجية ضمن الاختبار تشير إلى سلسلة التعليمات الرئيسية، فستطرح استثناءً أثناء اختبارات الوحدة.
في بعض الحالات، يمكنك إدخال مُرسِل Main
بالطريقة نفسها التي يتم بها حقن المُرسِلين الآخرين، كما هو موضَّح في القسم السابق، ما يسمح لك باستبداله بـ TestDispatcher
في الاختبارات. ومع ذلك، تستخدم بعض واجهات برمجة التطبيقات، مثل viewModelScope
، مرسِل Main
مشفّرًا بشكلٍ غير مرئي.
في ما يلي مثال على عملية تنفيذ ViewModel
تستخدم viewModelScope
لتشغيل الكوروتين الذي يُحمِّل البيانات:
class HomeViewModel : ViewModel() { private val _message = MutableStateFlow("") val message: StateFlow<String> get() = _message fun loadMessage() { viewModelScope.launch { _message.value = "Greetings!" } } }
لاستبدال مُرسِل Main
بـ TestDispatcher
في جميع الحالات، استخدِم الدالتين Dispatchers.setMain
وDispatchers.resetMain
.
class HomeViewModelTest { @Test fun settingMainDispatcher() = runTest { val testDispatcher = UnconfinedTestDispatcher(testScheduler) Dispatchers.setMain(testDispatcher) try { val viewModel = HomeViewModel() viewModel.loadMessage() // Uses testDispatcher, runs its coroutine eagerly assertEquals("Greetings!", viewModel.message.value) } finally { Dispatchers.resetMain() } } }
إذا تم استبدال مُرسِل Main
بـ TestDispatcher
، سيستخدم أي TestDispatchers
تم إنشاؤه حديثًا تلقائيًا أداة جدولة المهام من مُرسِل Main
، بما في ذلك StandardTestDispatcher
الذي تم إنشاؤه من قبل runTest
في حال عدم إرسال أي مرسِل آخر إليه.
يسهّل ذلك التأكد من استخدام نظام جدولة واحد فقط أثناء الاختبار. لتنفيذ ذلك، تأكَّد من إنشاء جميع مثيلات TestDispatcher
الأخرى بعد طلب Dispatchers.setMain
.
هناك نمط شائع لتجنُّب تكرار الرمز البرمجي الذي يحل محل مرسِل Main
في كل اختبار، وهو استخراجه إلى قاعدة اختبار JUnit:
// Reusable JUnit4 TestRule to override the Main dispatcher class MainDispatcherRule( val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(), ) : TestWatcher() { override fun starting(description: Description) { Dispatchers.setMain(testDispatcher) } override fun finished(description: Description) { Dispatchers.resetMain() } } class HomeViewModelTestUsingRule { @get:Rule val mainDispatcherRule = MainDispatcherRule() @Test fun settingMainDispatcher() = runTest { // Uses Main’s scheduler val viewModel = HomeViewModel() viewModel.loadMessage() assertEquals("Greetings!", viewModel.message.value) } }
تستخدم عملية تنفيذ القاعدة هذه UnconfinedTestDispatcher
بشكلٍ تلقائي، ولكن يمكن تمرير StandardTestDispatcher
كمَعلمة إذا لم يكن من المطلوب تنفيذ مُرسِل Main
في فئة اختبار معيّنة.
عندما تحتاج إلى مثيل TestDispatcher
في نص الاختبار، يمكنك إعادة استخدام testDispatcher
من القاعدة، ما دام هذا هو النوع المطلوب. إذا كنت تريد توضيح نوع TestDispatcher
المستخدم في الاختبار أو إذا كنت بحاجة إلى TestDispatcher
من نوع مختلف عن النوع Main
، يمكنك إنشاء TestDispatcher
جديد في runTest
. بما أنه تم ضبط مرسِل Main
على TestDispatcher
، سيشارك أي TestDispatchers
تم إنشاؤه حديثًا أداة جدولة المهام تلقائيًا.
class DispatcherTypesTest { @get:Rule val mainDispatcherRule = MainDispatcherRule() @Test fun injectingTestDispatchers() = runTest { // Uses Main’s scheduler // Use the UnconfinedTestDispatcher from the Main dispatcher val unconfinedRepo = Repository(mainDispatcherRule.testDispatcher) // Create a new StandardTestDispatcher (uses Main’s scheduler) val standardRepo = Repository(StandardTestDispatcher()) } }
إنشاء المرسلين خارج الاختبار
وفي بعض الحالات، قد تحتاج إلى توفير TestDispatcher
خارج طريقة الاختبار. على سبيل المثال، أثناء إعداد موقع في فئة الاختبار:
class Repository(private val ioDispatcher: CoroutineDispatcher) { /* ... */ } class RepositoryTestWithRule { private val repository = Repository(/* What TestDispatcher? */) @get:Rule val mainDispatcherRule = MainDispatcherRule() @Test fun someRepositoryTest() = runTest { // Test the repository... // ... } }
في حال استبدال مُرسِل "Main
" كما هو موضّح في القسم السابق، ستتم تلقائيًا مشاركة أداة جدولة مهام TestDispatchers
التي تم إنشاؤها بعد استبدال مرسِل Main
.
ومع ذلك، لا ينطبق ذلك على TestDispatchers
التي تم إنشاؤها كسمات لفئة الاختبار أو TestDispatchers
تم إنشاؤها أثناء إعداد السمات في فئة الاختبار. يتم إعداد هذه الرسائل قبل استبدال مُرسِل "Main
". لذلك، سيقوم بإنشاء برامج جدولة جديدة.
للتأكّد من وجود أداة جدولة واحدة فقط في الاختبار، أنشِئ السمة MainDispatcherRule
أولاً. وبعد ذلك، يمكنك إعادة استخدام مرسِلها (أو أداة الجدولة، إذا كنت بحاجة إلى TestDispatcher
من نوع مختلف) في أدوات إعداد المواقع الأخرى على مستوى الفئة حسب الحاجة.
class RepositoryTestWithRule { @get:Rule val mainDispatcherRule = MainDispatcherRule() private val repository = Repository(mainDispatcherRule.testDispatcher) @Test fun someRepositoryTest() = runTest { // Takes scheduler from Main // Any TestDispatcher created here also takes the scheduler from Main val newTestDispatcher = StandardTestDispatcher() // Test the repository... } }
يُرجى ملاحظة أنّ كلاً من runTest
وTestDispatchers
اللذين تم إنشاؤهما ضمن الاختبار سيظلان يشاركان تلقائيًا أداة جدولة مهام الإرسال Main
.
إذا لم تكن تريد استبدال مُرسِل Main
، أنشِئ أول TestDispatcher
(ما يؤدي إلى إنشاء أداة جدولة جديدة) كخاصية للفئة. وبعد ذلك، عليك تمرير أداة جدولة المهام يدويًا إلى كل استدعاء runTest
وكل TestDispatcher
جديد يتم إنشاؤه، سواء كخصائص أو ضمن الاختبار:
class RepositoryTest { // Creates the single test scheduler private val testDispatcher = UnconfinedTestDispatcher() private val repository = Repository(testDispatcher) @Test fun someRepositoryTest() = runTest(testDispatcher.scheduler) { // Take the scheduler from the TestScope val newTestDispatcher = UnconfinedTestDispatcher(this.testScheduler) // Or take the scheduler from the first dispatcher, they’re the same val anotherTestDispatcher = UnconfinedTestDispatcher(testDispatcher.scheduler) // Test the repository... } }
في هذا النموذج، يتم تمرير أداة الجدولة من المرسِل الأول إلى runTest
. سيؤدي هذا إلى إنشاء StandardTestDispatcher
جديد لـ TestScope
باستخدام برنامج جدولة المهام هذا. يمكنك أيضًا إرسال جهاز الإرسال إلى "runTest
" مباشرةً لتشغيل اختبار كوروتين على ذلك المُرسِل.
إنشاء نطاق TestScope
كما هي الحال في TestDispatchers
، قد تحتاج إلى الوصول إلى TestScope
خارج نص الاختبار. على الرغم من أنّ runTest
ينشئ TestScope
تلقائيًا، يمكنك أيضًا إنشاء TestScope
الخاص بك لاستخدامه مع runTest
.
عند إجراء ذلك، احرص على الاتصال بـ runTest
على TestScope
التي أنشأتها:
class SimpleExampleTest { val testScope = TestScope() // Creates a StandardTestDispatcher @Test fun someTest() = testScope.runTest { // ... } }
يُنشئ الرمز أعلاه StandardTestDispatcher
لـ TestScope
بشكل ضمني، بالإضافة إلى أداة جدولة جديدة. يمكن أيضًا إنشاء جميع هذه الكائنات بشكل صريح. ويمكن أن يكون هذا مفيدًا إذا كنت بحاجة إلى دمجه مع إعدادات إدخال التبعية.
class ExampleTest { val testScheduler = TestCoroutineScheduler() val testDispatcher = StandardTestDispatcher(testScheduler) val testScope = TestScope(testDispatcher) @Test fun someTest() = testScope.runTest { // ... } }
إدخال نطاق
إذا كان لديك فئة تنشئ الكوروتينات التي تحتاج إلى التحكّم فيها أثناء
الاختبارات، يمكنك إدخال نطاق الكوروتين في تلك الفئة واستبداله بـ
TestScope
في الاختبارات.
في المثال التالي، تعتمد الفئة UserState
على UserRepository
لتسجيل المستخدمين الجدد وجلب قائمة المستخدمين المسجَّلين. بما أنّ هذه الاستدعاءات الموجَّهة إلى UserRepository
يعلِّق استدعاءات الدوال، يستخدم UserState
العنصر CoroutineScope
الذي تم إدخاله لتشغيل الكوروتين الجديد داخل دالة registerUser
.
class UserState( private val userRepository: UserRepository, private val scope: CoroutineScope, ) { private val _users = MutableStateFlow(emptyList<String>()) val users: StateFlow<List<String>> = _users.asStateFlow() fun registerUser(name: String) { scope.launch { userRepository.register(name) _users.update { userRepository.getAllUsers() } } } }
لاختبار هذه الفئة، يمكنك تمرير TestScope
من runTest
عند إنشاء كائن UserState
:
class UserStateTest { @Test fun addUserTest() = runTest { // this: TestScope val repository = FakeUserRepository() val userState = UserState(repository, scope = this) userState.registerUser("Mona") advanceUntilIdle() // Let the coroutine complete and changes propagate assertEquals(listOf("Mona"), userState.users.value) } }
ولإدخال نطاق خارج دالة الاختبار، على سبيل المثال في كائن ضمن الاختبار الذي تم إنشاؤه كسمة في فئة الاختبار، يُرجى الاطّلاع على إنشاء نطاق TestScope.