اختبار الكوروتينات في لغة Kotlin على Android

يتطلب رمز اختبار الوحدة الذي يستخدم الكوروتينات مزيدًا من الاهتمام، حيث يمكن أن يكون تنفيذها غير متزامن ويتم تنفيذه على مستوى سلاسل محادثات متعددة. يتناول هذا الدليل كيفية اختبار دوال التعليق، وتصميمات الاختبار التي تحتاج إلى أن تكون على دراية بها، وكيفية جعل التعليمة البرمجية التي تستخدم الكوروتينات قابلة للاختبار.

تشكّل واجهات برمجة التطبيقات المستخدَمة في هذا الدليل جزءًا من مكتبة 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 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 الخاص بك:

مصادر إضافية