অ্যান্ড্রয়েডে কোটলিন কোরোটিন পরীক্ষা করা হচ্ছে

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 ব্যবহার করে) coroutine এক্সিকিউশনকে সরিয়ে দেয় তবে runTest এখনও সাধারণত কাজ করবে, কিন্তু বিলম্ব আর এড়িয়ে যাবে না, এবং একাধিক থ্রেডে কোড চালানোর কারণে পরীক্ষাগুলি কম অনুমানযোগ্য হবে। এই কারণে, পরীক্ষায় আপনার প্রকৃত প্রেরণকারীদের প্রতিস্থাপনের জন্য পরীক্ষা প্রেরকদের ইনজেকশন করা উচিত।

টেস্ট ডিসপ্যাচার

TestDispatchers হল পরীক্ষার উদ্দেশ্যে CoroutineDispatcher বাস্তবায়ন। নতুন করোটিনগুলিকে অনুমানযোগ্য করে তোলার জন্য পরীক্ষার সময় নতুন coroutines তৈরি করা হলে আপনাকে TestDispatchers ব্যবহার করতে হবে।

TestDispatcher এর দুটি উপলব্ধ বাস্তবায়ন রয়েছে: StandardTestDispatcher এবং UnconfinedTestDispatcher , যা নতুন শুরু হওয়া কোরোটিনগুলির বিভিন্ন সময়সূচী সম্পাদন করে। এই উভয়ই ভার্চুয়াল সময় নিয়ন্ত্রণ করতে এবং পরীক্ষার মধ্যে চলমান কোরোটিন পরিচালনা করতে একটি TestCoroutineScheduler ব্যবহার করে।

একটি পরীক্ষায় শুধুমাত্র একটি শিডিউলারের দৃষ্টান্ত ব্যবহার করা উচিত , যা সমস্ত TestDispatchers এর মধ্যে ভাগ করা হয়। শেয়ারিং শিডিউলার সম্পর্কে জানতে টেস্ট ডিসপ্যাচার ইনজেকশন করা দেখুন।

শীর্ষ-স্তরের পরীক্ষা করুটিন শুরু করতে, runTest একটি TestScope তৈরি করে, যা CoroutineScope এর একটি বাস্তবায়ন যা সর্বদা একটি TestDispatcher ব্যবহার করবে। নির্দিষ্ট করা না থাকলে, একটি TestScope ডিফল্টরূপে একটি StandardTestDispatcher তৈরি করবে, এবং এটিকে উচ্চ-স্তরের পরীক্ষা করুটিন চালানোর জন্য ব্যবহার করবে।

runTest তার TestScope এর প্রেরক দ্বারা ব্যবহৃত শিডিউলারে সারিবদ্ধ কোরোটিনগুলির ট্র্যাক রাখে এবং যতক্ষণ পর্যন্ত সেই শিডিউলারের কাজ মুলতুবি থাকে ততক্ষণ ফিরে আসবে না।

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

আপনি যখন একটি StandardTestDispatcher এ নতুন কোরোটিন শুরু করেন, তখন সেগুলি অন্তর্নিহিত শিডিউলারের সাথে সারিবদ্ধ থাকে, যখনই পরীক্ষার থ্রেডটি ব্যবহার করার জন্য বিনামূল্যে থাকে তখন চালানো হবে৷ এই নতুন coroutines চালানোর জন্য, আপনাকে পরীক্ষার থ্রেড দিতে হবে (অন্যান্য coroutines ব্যবহারের জন্য এটি মুক্ত করুন)। এই সারিবদ্ধ আচরণ আপনাকে পরীক্ষার সময় কীভাবে নতুন করোটিন চালানো হয় তার উপর সুনির্দিষ্ট নিয়ন্ত্রণ দেয় এবং এটি প্রোডাকশন কোডে করোটিনগুলির সময়সূচীর অনুরূপ।

টপ-লেভেল টেস্ট করুটিন চালানোর সময় যদি টেস্ট থ্রেড কখনও পাওয়া না যায়, তবে যেকোন নতুন কোরোটিন শুধুমাত্র টেস্ট করুটিন সম্পন্ন হওয়ার পরেই চলবে (কিন্তু 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 : বর্তমান ভার্চুয়াল সময়ে নির্ধারিত coroutines চালায়।

পূর্ববর্তী পরীক্ষাটি ঠিক করতে, 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 এ নতুন coroutines শুরু করা হয়, তখন সেগুলি বর্তমান থ্রেডে সাগ্রহে শুরু হয়। এর মানে হল যে তারা অবিলম্বে দৌড়ানো শুরু করবে, তাদের করুটিন নির্মাতার ফিরে আসার জন্য অপেক্ষা না করে। অনেক ক্ষেত্রে, এই প্রেরণের আচরণের ফলে সহজ পরীক্ষা কোড হয়, কারণ নতুন কোরোটিন চালানোর জন্য আপনাকে ম্যানুয়ালি পরীক্ষার থ্রেড দিতে হবে না।

যাইহোক, এই আচরণ আপনি অ-পরীক্ষা প্রেরণকারীদের সাথে উত্পাদনে যা দেখতে পাবেন তার থেকে আলাদা। যদি আপনার পরীক্ষা একযোগে ফোকাস করে, তাহলে পরিবর্তে 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 এ তাদের নতুন coroutines শুরু করবে, যার অর্থ হল লঞ্চের প্রতিটি কল শুধুমাত্র নিবন্ধন সম্পন্ন হওয়ার পরেই ফিরে আসবে।

মনে রাখবেন যে UnconfinedTestDispatcher সাগ্রহে নতুন coroutines শুরু করে, কিন্তু এর মানে এই নয় যে এটি সেগুলিকেও সাগ্রহে সম্পূর্ণ করার জন্য চালাবে। যদি নতুন কোরোটিন স্থগিত হয়, অন্য কোরোটিনগুলি আবার কার্যকর করা শুরু করবে।

উদাহরণস্বরূপ, এই পরীক্ষার মধ্যে চালু করা নতুন কোরোটিন অ্যালিসকে নিবন্ধন করবে, কিন্তু 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 ক্লাস দেখতে পাচ্ছেন যা 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 ইনজেক্ট করি এবং initialize হওয়ার আগে শুরু হওয়া নতুন coroutine সম্পূর্ণ হয়েছে কিনা তা নিশ্চিত করতে advanceUntilIdle ব্যবহার করি।

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 এ শুরু হওয়া নতুন coroutines ম্যানুয়ালি অগ্রসর করা যেতে পারে যেমনটি initialize দিয়ে উপরে দেখানো হয়েছে। উল্লেখ্য, যাইহোক, এটি উৎপাদন কোডে সম্ভব বা কাঙ্খিত হবে না। পরিবর্তে, এই পদ্ধতিটি হয় স্থগিত (অনুক্রমিক নির্বাহের জন্য), অথবা একটি Deferred মান ফেরত দেওয়ার জন্য (সমসাময়িক সম্পাদনের জন্য) পুনরায় ডিজাইন করা উচিত।

উদাহরণস্বরূপ, আপনি একটি নতুন coroutine শুরু করতে এবং একটি 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())
    // ...
}

runTest মুলতুবি থাকা কোরোটিনগুলি পূরণ করার জন্য অপেক্ষা করবে যদি কোরোটিনগুলি একটি TestDispatcher থাকে যার সাথে এটি একটি শিডিউলার ভাগ করে। এটি কর্উটিনগুলির জন্যও অপেক্ষা করবে যেগুলি শীর্ষ-স্তরের পরীক্ষা করুটিনের সন্তান, এমনকি যদি তারা অন্য প্রেরণকারীদের ( dispatchTimeoutMs প্যারামিটার দ্বারা নির্দিষ্ট একটি টাইমআউট পর্যন্ত, যা ডিফল্টরূপে 60 সেকেন্ড)।

প্রধান প্রেরণকারী সেট করা হচ্ছে

স্থানীয় ইউনিট পরীক্ষায় , Android UI থ্রেড মোড়ানো Main প্রেরণকারী অনুপলব্ধ হবে, কারণ এই পরীক্ষাগুলি একটি স্থানীয় JVM-এ সম্পাদিত হয়, Android ডিভাইসে নয়৷ যদি পরীক্ষার অধীনে আপনার কোডটি মূল থ্রেডের উল্লেখ করে, তবে এটি ইউনিট পরীক্ষার সময় একটি ব্যতিক্রম নিক্ষেপ করবে।

কিছু ক্ষেত্রে, আপনি Main প্রেরণকারীকে অন্যান্য প্রেরকদের মতো একইভাবে ইনজেকশন করতে পারেন, যেমনটি পূর্ববর্তী বিভাগে বর্ণিত হয়েছে, আপনাকে পরীক্ষায় এটিকে একটি TestDispatcher দিয়ে প্রতিস্থাপন করার অনুমতি দেয়। যাইহোক, কিছু API যেমন viewModelScope হুডের নিচে একটি হার্ডকোড করা Main প্রেরক ব্যবহার করে।

এখানে একটি ViewModel বাস্তবায়নের একটি উদাহরণ রয়েছে যা viewModelScope ব্যবহার করে একটি কোরোটিন চালু করতে যা ডেটা লোড করে:

class HomeViewModel : ViewModel() {
    private val _message = MutableStateFlow("")
    val message: StateFlow<String> get() = _message

    fun loadMessage() {
        viewModelScope.launch {
            _message.value = "Greetings!"
        }
    }
}

সমস্ত ক্ষেত্রে একটি TestDispatcher দিয়ে Main প্রেরণকারীকে প্রতিস্থাপন করতে, 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 এর ধরন সম্পর্কে স্পষ্ট হতে চান, অথবা আপনার যদি একটি 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 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 প্রেরণকারীকে প্রতিস্থাপন করেন, তবে Main প্রেরণকারী প্রতিস্থাপিত হওয়ার পরে তৈরি TestDispatchers স্বয়ংক্রিয়ভাবে তার সময়সূচী ভাগ করবে।

তবে, টেস্ট ক্লাসের বৈশিষ্ট্য হিসাবে তৈরি করা 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 পাস করা হয়। এটি সেই সময়সূচী ব্যবহার করে TestScope জন্য একটি নতুন StandardTestDispatcher তৈরি করবে। আপনি সেই প্রেরণকারীতে পরীক্ষা করুটিন চালানোর জন্য সরাসরি runTest প্রেরণকারীতে পাস করতে পারেন।

আপনার নিজস্ব টেস্টস্কোপ তৈরি করা

TestDispatchers এর মতো, আপনাকে পরীক্ষার মূল অংশের বাইরে একটি TestScope অ্যাক্সেস করতে হতে পারে। runTest স্বয়ংক্রিয়ভাবে হুডের নিচে একটি TestScope তৈরি করে, আপনি runTest সাথে ব্যবহার করার জন্য আপনার নিজস্ব TestScope ও তৈরি করতে পারেন।

এটি করার সময়, আপনার তৈরি করা TestScoperunTest কল করতে ভুলবেন না:

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 তার registerUser ফাংশনের ভিতরে একটি নতুন coroutine শুরু করতে ইনজেকশন করা CoroutineScope ব্যবহার করে৷

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 তৈরি করা

অতিরিক্ত সম্পদ