تتطلب شفرة اختبار الوحدة التي تستخدم كوروتينات قدرًا من الاهتمام الإضافي، نظرًا لأن تنفيذها قد يكون غير متزامن ويتم عبر سلاسل محادثات متعددة. يتناول هذا الدليل كيفية اختبار وظائف التعليق وتركيبات الاختبار التي عليك أن تكون على دراية بها وكيفية جعل الرمز الذي يستخدم الكوروتينات قابلاً للاختبار.
تُعد واجهات برمجة التطبيقات المستخدمة في هذا الدليل جزءًا من مكتبة 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
{0}{/0}. - إذا نقلت الشفرة تنفيذ عملية كوروتين إلى مُرسلين آخرين (على سبيل المثال، باستخدام
withContext
)، فسيستمر عملrunTest
بشكل عام، ولكن لن يتم تخطي أي تأخير بعد ذلك، وسيتم إجراء الاختبارات. أقل قابلية للتوقع حيث تعمل الشفرة على سلاسل محادثات متعددة. لهذه الأسباب، في الاختبارات، عليك إدخال مُرسِلين تجريبيين بدلاً من مُرسلِين حقيقيين.
مُرسِلو الاختبار
TestDispatchers
هي عمليات تنفيذ CoroutineDispatcher
لأغراض الاختبار. ستحتاج إلى استخدام TestDispatchers
إذا تم إنشاء الكوروتينات الجديدة أثناء الاختبار لجعل تنفيذ الكوروتينات الجديدة يمكن التنبؤ بها.
يتوفّر تنفيذان لـ TestDispatcher
: StandardTestDispatcher
وUnconfinedTestDispatcher
، ويؤديا عملية جدولة مختلفة للكوروتينات التي تم بدؤها حديثًا. يستخدم كلاهما TestCoroutineScheduler
للتحكّم في الوقت الافتراضي وإدارة الكوروتينات قيد التشغيل ضمن الاختبار.
ينبغي استخدام مثيل أداة جدولة واحد فقط في الاختبار، وأن تكون مشتركة بين كل TestDispatchers
. يُرجى الاطّلاع على المقالة Injecting TestDispatchers للتعرّف على مشاركة برامج الجدولة.
لبدء تشغيل الكوروتين التجريبي من المستوى الأعلى،runTest
ينشئTestScope
، وهو تنفيذ لـCoroutineScope
الذي سيستخدم دائمًاTestDispatcher
{0}{/0}. وفي حال عدم تحديد هذه السمة، ستنشئ 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
، يتم تشغيلها بشغف على سلسلة المحادثات الحالية. وهذا يعني أنه سيبدأ عرضها على الفور، بدون انتظار عودة أداة إنشاء الكوروتين. وفي كثير من الحالات، ينتج عن سلوك الإرسال هذا شفرة اختبار أبسط، حيث لا تحتاج إلى تقديم سلسلة الاختبار يدويًا للسماح بتشغيل الكوروتينات الجديدة.
ومع ذلك، يختلف هذا السلوك عن ما ستراه في مرحلة الإنتاج مع مُرسلين غير تجريبيين. إذا كان الاختبار يركّز على التزامن، يُفضَّل استخدام 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
التي تنشئ مستوى coroutine جديدًا باستخدام المرسِل 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
{0}{/0}. بما أنه تم ضبط المُرسل 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
مباشرةً لتشغيل كوروتين الاختبار على ذلك المُرسل.
إنشاء نطاق الاختبار الخاص بك
كما هو الحال مع 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) } }
لإدخال نطاق خارج دالة الاختبار، على سبيل المثال، في عنصر ضمن الاختبار تم إنشاؤه كخاصيّة في فئة الاختبار، راجع إنشاء نطاق تجريبي خاص بك.