يتطلب رمز اختبار الوحدة الذي يستخدم الكوروتينات مزيدًا من الاهتمام، حيث يمكن أن يكون تنفيذها غير متزامن ويتم تنفيذه على مستوى سلاسل محادثات متعددة. يتناول هذا الدليل كيفية اختبار دوال التعليق، وتصميمات الاختبار التي تحتاج إلى أن تكون على دراية بها، وكيفية جعل التعليمة البرمجية التي تستخدم الكوروتينات قابلة للاختبار.
تشكّل واجهات برمجة التطبيقات المستخدَمة في هذا الدليل جزءًا من مكتبة 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
. اطلع على إدخال TestDispatchers لمعرفة المزيد حول مشاركة برامج الجدولة.
لبدء اختبار الكوروتينات ذات المستوى الأعلى، ينشئ runTest
خوارزمية TestScope
، وهي تنفّذ السمة CoroutineScope
التي ستستخدم دائمًا TestDispatcher
. في حال عدم تحديد هذه السمة، سينشئ TestScope
تلقائيًا StandardTestDispatcher
ويستخدمه لتشغيل الكوروتين الاختباري عالي المستوى.
يتتبّع "runTest
" الكوروتينات التي تم وضعها في قائمة الانتظار على أداة الجدولة التي يستخدمها المرسِل لنظام TestScope
، ولن يعود ما دام هناك عمل في انتظار المراجعة على أداة الجدولة هذه.
مرسِل اختبار عادي
عند بدء سلسلة الكوروتينات الجديدة على 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 ExampleRepository(private val ioDispatcher: CoroutineDispatcher) { /* ... */ } class RepositoryTestWithRule { private val repository = ExampleRepository(/* 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 = ExampleRepository(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 = ExampleRepository(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 الخاص بك: