Android पर Kotlin कोरूटीन की जांच करना

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 के बीच शेयर किया जाता है. शेड्यूलर शेयर करने के बारे में जानने के लिए, 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 पर नए कोरूटीन शुरू होते हैं, तो वे मौजूदा थ्रेड पर तेज़ी से शुरू हो जाते हैं. इसका मतलब है कि वे अपने कोरूटीन बनाने वाले के वापस आने का इंतज़ार किए बिना, तुरंत चलना शुरू कर देंगे. कई मामलों में, इस डिस्पैच करने के तरीके की वजह से टेस्ट कोड आसान हो जाता है, क्योंकि आपको नए कोरूटीन चलाने के लिए, मैन्युअल तौर पर टेस्ट थ्रेड को जनरेट करने की ज़रूरत नहीं होती.

हालांकि, यह व्यवहार आपको प्रोडक्शन में गैर-जांच करने वाले लोगों के साथ अलग तरह से दिखेगा. अगर टेस्ट में कई चीज़ों को एक साथ इस्तेमाल करने पर फ़ोकस होता है, तो 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 के इंस्टेंस से बदलें. इसके कई फ़ायदे हैं:

  • यह कोड, सिंगल टेस्ट थ्रेड पर चलेगा. इससे टेस्ट ज़्यादा डिटरमिनिस्टिक होगा
  • आपके पास यह कंट्रोल करने का विकल्प है कि नए कोरूटीन को शेड्यूल और एक्ज़ीक्यूट कैसे किया जाए
  • टेस्ट डिस्पैचर, वर्चुअल टाइम के लिए शेड्यूलर का इस्तेमाल करते हैं. इसमें, देरी नहीं होती और आपको मैन्युअल तरीके से समय को आगे बढ़ाने की सुविधा मिलती है

डिपेंडेंसी इंजेक्शन का इस्तेमाल करके, आपकी क्लास के ट्रांसपैचर की मदद से, असली डिसपैचर को बदलना आसान हो जाता है टेस्ट. इन उदाहरणों में, हम CoroutineDispatcher इंजेक्ट करेंगे, लेकिन यह भी किया जा सकता है पूरी दुनिया को इंजेक्ट करें CoroutineContext टाइप किया गया है, जो टेस्ट के दौरान ज़्यादा विकल्प देता है.

कोरूटीन शुरू करने वाली क्लास के लिए, एक CoroutineScope को इंजेक्ट किया जा सकता है के बजाय, जैसा कि स्कोप इंजेक्ट करना में बताया गया है सेक्शन में जाएं.

जब TestDispatchers को इंस्टैंशिएट किया जाएगा, तो डिफ़ॉल्ट रूप से एक नया शेड्यूलर बन जाएगा. runTest में, आप TestScope की testScheduler प्रॉपर्टी को ऐक्सेस कर सकते हैं और इसे किसी भी नए बनाए गए TestDispatchers में पास कर सकते हैं. इससे वर्चुअल टाइम के बारे में उनकी समझ को शेयर किया जाएगा. साथ ही, advanceUntilIdle जैसे तरीकों से सभी टेस्ट डिसपैचर को कोरूटीन पूरा होगा.

इस उदाहरण में, आपको ऐसी Repository क्लास दिख सकती है जो IO डिस्पैचर की initialize तरीके में एक नया कोरूटीन बनाती है. साथ ही, यह कॉलर को उसकी fetchData तरीके में IO डिसपैचर पर स्विच करती है:

// 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"
    }
}

टेस्ट में, IO डिसपैचर की जगह लेने के लिए, TestDispatcher इंजेक्ट किया जा सकता है.

नीचे दिए गए उदाहरण में, हम डेटा स्टोर करने की जगह में 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 वैल्यू दिखा सके (एक साथ चल रहे एक्ज़ीक्यूशन के लिए).

उदाहरण के लिए, नया कोरूटीन शुरू करने और एक Deferred बनाने के लिए, async का इस्तेमाल किया जा सकता है:

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())
    // ...
}

अगर कोरूटीन किसी ऐसे TestDispatcher पर हों जिसके साथ शेड्यूलर शेयर किया गया हो, तो runTest कोरूटीन वापस आने से पहले उनके पूरा होने का इंतज़ार करती है. यह ऐसे कोरूटीन के लिए भी इंतज़ार करेगा जो टॉप-लेवल टेस्ट कोरूटीन के चाइल्ड हैं. भले ही, वे दूसरे डिसपैचर पर हों (डिफ़ॉल्ट रूप से 60 सेकंड, यानी कि dispatchTimeoutMs पैरामीटर से तय किए गए टाइम आउट तक).

मुख्य डिसपैचर सेट करना

लोकल यूनिट टेस्ट में, Android यूज़र इंटरफ़ेस (यूआई) थ्रेड को रैप करने वाला Main डिसपैचर उपलब्ध नहीं होगा, क्योंकि ये टेस्ट Android डिवाइस पर नहीं, बल्कि लोकल JVM पर किए जाते हैं. अगर जांच में शामिल आपके कोड में मुख्य थ्रेड का रेफ़रंस दिया गया है, तो यूनिट जांच के दौरान इसे अपवाद माना जाएगा.

कुछ मामलों में, पिछले सेक्शन में बताए गए तरीके की तरह, 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 डिस्पैचर से शेड्यूलर का अपने-आप इस्तेमाल करेगा. इसमें runTest का बनाया गया StandardTestDispatcher भी शामिल है. ऐसा तब होगा, जब इसको कोई और डिस्पैचर पास नहीं किया जाएगा.

इससे यह पक्का करना आसान हो जाता है कि टेस्ट के दौरान सिर्फ़ एक शेड्यूलर का इस्तेमाल किया जा रहा है. यह सुविधा काम करे, इसके लिए पक्का करें कि Dispatchers.setMain को कॉल करने के बाद, अन्य TestDispatcher इंस्टेंस बनाए जाएं.

हर जांच में 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 का इस्तेमाल किया जाता है. हालांकि, अगर Main डिसपैचर को किसी टेस्ट क्लास में तेज़ी से कार्रवाई नहीं करनी है, तो StandardTestDispatcher को पैरामीटर के तौर पर पास किया जा सकता है.

जब आपको जांच के मुख्य भाग में TestDispatcher इंस्टेंस की ज़रूरत हो, तो नियम में मौजूद testDispatcher का फिर से इस्तेमाल किया जा सकता है. बशर्ते, यह आपकी पसंद के मुताबिक हो. अगर आपको जांच में इस्तेमाल किए गए TestDispatcher के टाइप के बारे में साफ़ तौर पर जानकारी देनी है या अगर आपको कोई ऐसा TestDispatcher चाहिए जो Main के लिए इस्तेमाल किए गए तरीके से अलग हो, तो runTest में नया TestDispatcher बनाएं. 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 डिस्पैचर को बदला जा रहा है, तो Main डिस्पैचर को बदलने के बाद बनाया गया TestDispatchers, अपना शेड्यूलर अपने-आप शेयर कर देगा.

हालांकि, यह बात जांच क्लास की प्रॉपर्टी के तौर पर बनाई गई 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 को भेजा जाता है. ऐसा करने पर, उस शेड्यूलर का इस्तेमाल करके TestScope के लिए एक नया StandardTestDispatcher बन जाएगा. उस डिस्पैचर पर टेस्ट कोरूटीन चलाने के लिए, डिसपैचर को सीधे runTest को भी पास किया जा सकता है.

अपना खुद का TestScope बनाना

TestDispatchers की तरह ही, आपको जांच के मुख्य हिस्से के बाहर मौजूद TestScope को ऐक्सेस करना पड़ सकता है. runTest अपने-आप हुड के तहत TestScope बनाता है. हालांकि, runTest के साथ इस्तेमाल करने के लिए, अपना TestScope भी बनाया जा सकता है.

ऐसा करते समय, अपने बनाए गए TestScope पर runTest को कॉल करना न भूलें:

class SimpleExampleTest {
    val testScope = TestScope() // Creates a StandardTestDispatcher

    @Test
    fun someTest() = testScope.runTest {
        // ...
    }
}

ऊपर दिया गया कोड, सीधे तौर पर TestScope के लिए एक StandardTestDispatcher और एक नया शेड्यूलर बनाता है. इन सभी ऑब्जेक्ट को अलग से भी बनाया जा सकता है. अगर आपको इसे डिपेंडेंसी इंजेक्शन सेटअप के साथ इंटिग्रेट करना है, तो यह काफ़ी मददगार हो सकता है.

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 बनाना.

अन्य संसाधन