Android-এ Kotlin coroutines পরীক্ষা করা

করুটিন ব্যবহার করে এমন কোডের ইউনিট টেস্টিংয়ের জন্য কিছু অতিরিক্ত মনোযোগের প্রয়োজন, কারণ এগুলি অ্যাসিঙ্ক্রোনাসভাবে এক্সিকিউট হতে পারে এবং একাধিক থ্রেড জুড়ে ঘটতে পারে। এই গাইড থেকে আপনি জানতে পারবেন কীভাবে সাসপেন্ড করা ফাংশন পরীক্ষা করা যায়, কোন কোন টেস্টিং কনস্ট্রাক্ট সম্পর্কে আপনাকে ওয়াকিবহাল থাকতে হবে এবং কীভাবে আপনার কোডকে করোটিন ব্যবহার করে টেস্ট করার উপযুক্ত করে তুলতে হয়।

এই নির্দেশিকায় ব্যবহৃত API, kotlinx.coroutines.test লাইব্রেরির অংশ। এইসব API অ্যাক্সেস করতে, আপনার প্রোজেক্টে টেস্টের উপর নির্ভরশীলতা হিসেবে আর্টিফ্যাক্ট যোগ করতে ভুলবেন না।

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

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

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-এর মধ্যে, আপনি TestScope-এর testScheduler প্রপার্টি অ্যাক্সেস করতে এবং যেকোনও নতুন তৈরি করা TestDispatchers-এ পাস করতে পারবেন। এর ফলে ভার্চুয়াল টাইম সম্পর্কে তাদের বোঝাপড়া শেয়ার করা হবে এবং advanceUntilIdle-এর মতো পদ্ধতি সব টেস্ট ডিসপ্যাচারদের উপর কোরাউটিন সম্পূর্ণ করার জন্য রান করবে।

নিচের উদাহরণে, আপনি এমন একটি Repository ক্লাস দেখতে পাবেন যা সেটির initialize পদ্ধতিতে IO ডিসপ্যাচার ব্যবহার করে একটি নতুন কোরাউটিন তৈরি করে এবং সেটির 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 ভ্যালু (কনকারেন্ট এক্সিকিউশনের জন্য) রিটার্ন করা যায়।

যেমন, আপনি নতুন কোরাউটিন শুরু করতে 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 প্যারামিটার দ্বারা নির্দিষ্ট করা টাইমআউট পর্যন্ত, যা ডিফল্ট হিসেবে ৬০ সেকেন্ড)।

প্রধান ডিসপ্যাচার সেট করা

লোকাল ইউনিট টেস্টে, Android UI থ্রেড র‍্যাপ করা Main ডিসপ্যাচার উপলভ্য থাকবে না, কারণ এইসব টেস্ট লোকাল JVM-এ এক্সিকিউট করা হয়, Android ডিভাইসে নয়। পরীক্ষা করা কোডটি যদি মূল থ্রেডকে রেফারেন্স করে, তাহলে ইউনিট টেস্টের সময় এটি একটি ব্যতিক্রম তৈরি করবে।

কিছু ক্ষেত্রে, আপনি আগের বিভাগে বর্ণিত অন্যান্য ডিসপ্যাচারদের মতো একই উপায়ে Main ডিসপ্যাচার ইনজেক্ট করতে পারেন, যা আপনাকে পরীক্ষার সময় TestDispatcher দিয়ে এটি প্রতিস্থাপন করতে দেয়। তবে, viewModelScope-এর মতো কিছু API-তে 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 ব্যবহার করা হয়, কিন্তু StandardTestDispatcher-কে প্যারামিটার হিসেবে পাস করা যেতে পারে যদি Main ডিসপ্যাচারকে কোনও নির্দিষ্ট টেস্ট ক্লাসে অবিলম্বে এক্সিকিউট না করতে হয়।

টেস্ট বডিতে TestDispatcher ইনস্ট্যান্সের প্রয়োজন হলে, সেটি কাঙ্ক্ষিত ধরনের হলে, আপনি নিয়মের testDispatcher আবার ব্যবহার করতে পারবেন। আপনি যদি টেস্টে ব্যবহৃত TestDispatcher-এর ধরন সম্পর্কে স্পষ্ট হতে চান অথবা Main-এর জন্য ব্যবহৃত TestDispatcher-এর থেকে আলাদা ধরনের TestDispatcher-এর প্রয়োজন হয়, তাহলে 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() }
        }
    }
}

এই ক্লাস পরীক্ষা করে দেখতে, UserState অবজেক্ট তৈরি করার সময় আপনি runTest থেকে TestScope পাস করতে পারেন:

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 তৈরি করা দেখুন।

অতিরিক্ত রিসোর্স