v2 টেস্টিং API-এর মাধ্যমে টেস্ট এনভায়রনমেন্ট কনফিগার করা

Compose টেস্টিং API-এর v2 ভার্সন (createComposeRule, createAndroidComposeRule, runComposeUiTest, runAndroidComposeUiTest, ইত্যাদি) এখন উপলভ্য। এর মাধ্যমে কোরাউটিন এক্সিকিউশনের উপর আরও ভাল কন্ট্রোল পাওয়া যাবে। এই আপডেট সম্পূর্ণ API সারফেস ডুপ্লিকেট করে না; শুধুমাত্র যে API টেস্ট এনভায়রনমেন্ট তৈরি করে তা আপডেট করা হয়েছে।

v1 API বন্ধ করে দেওয়া হয়েছে এবং নতুন API-তে মাইগ্রেট করার জন্য বিশেষভাবে সাজেস্ট করা হয়। মাইগ্রেট করলে আপনার পরীক্ষাগুলি স্ট্যান্ডার্ড কোরাউটিন আচরণ মেনে চলে এবং ভবিষ্যতে মানানসই হওয়া সংক্রান্ত সমস্যা এড়ানো যায়। অননুমোদিত v1 API-এর তালিকা দেখতে, API ম্যাপিং দেখুন।

v1 API UnconfinedTestDispatcher-এর উপর নির্ভর করলেও, v2 API রান করা কম্পোজিশনের জন্য ডিফল্ট হিসেবে StandardTestDispatcher ব্যবহার করে। এই পরিবর্তন স্ট্যান্ডার্ড runTest API-এর সাথে Compose টেস্টের আচরণকে মানানসই করে এবং কোরুটিন এক্সিকিউশন অর্ডারের উপর স্পষ্ট কন্ট্রোল প্রদান করে।

টেস্ট এনভায়রনমেন্ট কনফিগার করা

Compose test v2 API, টেস্ট এনভায়রনমেন্ট কাস্টমাইজ করতে ComposeUiTestConfig ব্যবহার করে। টেস্টের জন্য সেট-আপ ফাংশন তৈরি করে এমন API, যেমন createComposeRule, runComposeUiTest এবং অন্যান্য সম্পর্কিত API, ComposeUiTestConfig গ্রহণ করে। এই কনফিগারেশন অবজেক্ট effectContext, runTestContext, এবং testTimeout-এর মতো পরিবেশ সংক্রান্ত API-কে একটি অবজেক্টে একত্রিত করে।

কনফিগারেশন মডেল inputMode-ও ম্যানেজ করে। Compose test v2 API প্রতিটি টেস্টের শুরুতে ডিফল্ট হিসেবে InputMode.Touch এনফোর্স করে ডেটারমিনিজম নিশ্চিত করে এবং টেস্টের মধ্যে ইনপুট মোড স্টেট লিকেজ প্রতিরোধ করে।

ComposeUiTestConfig হল Compose testing v2 API-এর অংশ, যা ডিফল্ট হিসেবে StandardTestDispatcher ব্যবহার করে। আপনার পরীক্ষায় v1 API ব্যবহার করা হলে, ComposeUiTestConfig গ্রহণ করার আগে v2 টেস্টিং API-তে মাইগ্রেট করুন লিঙ্কটি দেখুন।

ComposeUiTestConfig-এ মাইগ্রেট করুন

টেস্টে সেট-আপ ফাংশন তৈরি করার জন্য ওভারলোডের মধ্যে, আলাদা আলাদা কনফিগারেশন প্যারামিটার গ্রহণ করে এমন একাধিক ওভারলোড -- যেমন effectContext, runTestContext বা testTimeout -- সরিয়ে দেওয়া হয়েছে। নিচে দেওয়া উদাহরণে দেখানো ComposeUiTestConfig-এর পরিবর্তে আপনার টেস্ট আপডেট করুন:

ডিফল্ট ইনপুট মোড

টেস্ট শুরু হওয়ার আগে ইন্সট্রুমেন্টেশন API-এর মাধ্যমে কনফিগার করা নন-টাচ ইনপুট মোডের উপর নির্ভর করলে, মাইগ্রেশনের সময় টেস্ট ব্যর্থ হতে পারে। টেস্টের জন্য সেট-আপ ফাংশনের মধ্যে, সিস্টেম আরও বেশি ডিটারমিনিজম এবং স্টেট লিকেজ প্রতিরোধ করতে, অ্যাম্বিয়েন্ট ডিভাইস স্টেট এবং প্রি-টেস্ট সেট-আপ ওভাররাইড করে প্রতিটি টেস্টের শুরুতে ডিফল্ট হিসেবে InputMode.Touch প্রয়োগ করে।

এটি সমাধান করতে, ComposeUiTestConfig-এ প্রয়োজনীয় ইনপুট মোড নির্দিষ্ট করুন:

class FocusTest {
    @get:Rule
    val rule = createComposeRule(
        config = ComposeUiTestConfig(inputMode = InputMode.Keyboard)
    )

    @Test
    fun testFocus() {}
}

সম্পূর্ণ টেস্ট ক্লাসের পরিবর্তে স্বতন্ত্র টেস্ট কেসের জন্য ইনপুট মোড কনফিগার করতে, ComposeUiTestConfig থেকে runComposeUiTest পাস করুন:

class FocusTest {
    @Test
    fun testTouchMode() = runComposeUiTest {
        // Runs with the default InputMode.Touch
    }

    @Test
    fun testKeyboardMode() = runComposeUiTest(
        ComposeUiTestConfig(inputMode = InputMode.Keyboard)
    ) {
        // Runs with InputMode.Keyboard
    }
}

অন্যান্য মাইগ্রেশন সংক্রান্ত সমস্যা ও তার সমাধান সম্পর্কে জানতে, সাধারণ সমস্যা ও কীভাবে তা সমাধান করবেন দেখুন।

v2 টেস্টিং API-তে মাইগ্রেট করা

v2 API-তে আপগ্রেড করার সময়, আপনি সাধারণত খুঁজুন + পরিবর্তন করুন ব্যবহার করে প্যাকেজ ইমপোর্ট আপডেট করতে এবং নতুন ডিসপ্যাচার পরিবর্তন গ্রহণ করতে পারবেন।

অথবা, নিম্নলিখিত প্রম্পট ব্যবহার করে Gemini-কে Compose টেস্টিং API-এর v2-তে মাইগ্রেশন করতে বলুন:

v1 টেস্টিং API থেকে v2 টেস্টিং API-তে মাইগ্রেট করা

v2 টেস্টিং API-তে মাইগ্রেট করার জন্য এই প্রম্পট এই গাইড ব্যবহার করবে।

Migrate to Compose testing v2 APIs using the official
migration guide.

AI প্রম্পট ব্যবহার করা

AI প্রম্পট Android Studio-তে Gemini-র মধ্যে ব্যবহার করার জন্য তৈরি করা হয়েছে।

Studio-তে Gemini সম্পর্কে আরও জানুন: https://developer.android.com/studio/gemini/overview

বন্ধ হয়ে যাওয়া v1 API-কে তার v2 রিপ্লেসমেন্টের সাথে ম্যাপ করতে, নিম্নলিখিত টেবিল ব্যবহার করুন:

বন্ধ করে দেওয়া হয়েছে (v1)

পরিবর্ত (v2)

androidx.compose.ui.test.junit4.createComposeRule

androidx.compose.ui.test.junit4.v2.createComposeRule

androidx.compose.ui.test.junit4.createAndroidComposeRule

androidx.compose.ui.test.junit4.v2.createAndroidComposeRule

androidx.compose.ui.test.junit4.createEmptyComposeRule

androidx.compose.ui.test.junit4.v2.createEmptyComposeRule

androidx.compose.ui.test.junit4.AndroidComposeTestRule

androidx.compose.ui.test.junit4.v2.AndroidComposeTestRule

androidx.compose.ui.test.runComposeUiTest

androidx.compose.ui.test.v2.runComposeUiTest

androidx.compose.ui.test.runAndroidComposeUiTest

androidx.compose.ui.test.v2.runAndroidComposeUiTest

androidx.compose.ui.test.runEmptyComposeUiTest

androidx.compose.ui.test.v2.runEmptyComposeUiTest

androidx.compose.ui.test.AndroidComposeUiTestEnvironment

androidx.compose.ui.test.v2.AndroidComposeUiTestEnvironment

পুরনো ভার্সনের সাথে মানানসই হওয়া ও ব্যতিক্রম

আগেকার v1 API এখন আর ব্যবহার করা যায় না, তবে আগেকার আচরণ বজায় রাখতে এবং পরিবর্তন আটকানোর জন্যUnconfinedTestDispatcher ব্যবহার করা চালিয়ে যান।

ডিফল্ট আচরণ পরিবর্তন করা হয়েছে এমন একমাত্র ব্যতিক্রম হল:

AndroidComposeUiTestEnvironment ক্লাসে কম্পোজিশন চালানোর জন্য ব্যবহৃত ডিফল্ট টেস্ট ডিসপ্যাচার UnconfinedTestDispatcher থেকে StandardTestDispatcher-এ পরিবর্তন করা হয়েছে। এর ফলে সেইসব ক্ষেত্রে প্রভাব পড়ে যেখানে আপনি কনস্ট্রাক্টর ব্যবহার করে কোনও ইনস্ট্যান্স তৈরি করেন অথবা সাবক্লাস AndroidComposeUiTestEnvironment তৈরি করেন এবং সেই কনস্ট্রাক্টরকে কল করেন।

মূল পরিবর্তন: করুটিন এক্সিকিউশনের উপর প্রভাব

API-এর v1 ও v2-এর মধ্যে প্রধান পার্থক্য হল, কীভাবে কোরাউটিন ডিসপ্যাচ করা হয়:

  • v1 API (UnconfinedTestDispatcher): কোনও করুটিন লঞ্চ করা হলে, সেটি বর্তমান থ্রেডে সঙ্গে সঙ্গে এক্সিকিউট হয়ে যেত, প্রায়শই পরীক্ষার কোডের পরবর্তী লাইন রান করার আগেই সেটি সম্পূর্ণ হয়ে যেত। প্রোডাকশন আচরণের বিপরীতে, এই অবিলম্বে এক্সিকিউশন অনিচ্ছাকৃতভাবে আসল টাইমিং সংক্রান্ত সমস্যা বা রেস কন্ডিশন আড়াল করতে পারে যা লাইভ অ্যাপ্লিকেশনে ঘটে।
  • v2 API (StandardTestDispatcher): কোনও করুটিন লঞ্চ করা হলে, সেটি সারিভুক্ত করা হয় এবং টেস্ট স্পষ্টভাবে ভার্চুয়াল ঘড়ি অ্যাডভান্স না করা পর্যন্ত সেটি এক্সিকিউট হয় না। স্ট্যান্ডার্ড কম্পোজ টেস্ট API (যেমন waitForIdle()) আগেই এই সিঙ্ক্রোনাইজেশন ম্যানেজ করে, তাই এইসব স্ট্যান্ডার্ড API-এর উপর নির্ভর করা বেশিরভাগ টেস্ট কোনও পরিবর্তন ছাড়াই কাজ করা চালিয়ে যেতে পারবে।

সাধারণ সমস্যা ও সেগুলি কীভাবে সমাধান করতে হয়

v2-তে আপগ্রেড করার পরে আপনার পরীক্ষাগুলি ব্যর্থ হলে, সেগুলি সম্ভবত নিম্নলিখিত প্যাটার্ন দেখাবে:

  • ব্যর্থ: আপনি কোনও টাস্ক লঞ্চ করেছেন (যেমন, ViewModel ডেটা লোড করে), কিন্তু আপনার অ্যাসার্শন সঙ্গে সঙ্গে ব্যর্থ হয়ে গেছে, কারণ ডেটা এখনও "লোডিং" অবস্থায় আছে।
  • কারণ: v2 API-এর ক্ষেত্রে, কোরাউটিনকে অবিলম্বে এক্সিকিউট না করে কিউতে রাখা হয়। টাস্কটি সারিতে ছিল কিন্তু ফলাফল চেক করার আগে সেটি কখনও প্রকৃতপক্ষে রান করেনি।
  • সমাধান: সময়কে স্পষ্টভাবে এগিয়ে দিন। কখন কাজ এক্সিকিউট করতে হবে তা আপনাকে v2 ডিসপ্যাচারকে স্পষ্টভাবে জানাতে হবে।

আগের পদ্ধতি

v1-এ, টাস্ক লঞ্চ হয়ে সঙ্গে সঙ্গে শেষ হয়ে যেত। v2-তে, নিম্নলিখিত কোড কাজ করে না কারণ loadData() এখনও রান করেনি।

// In v1, this launched and finished immediately.
viewModel.loadData()

// In v2, this fails because loadData() hasn't actually run yet!
assertEquals(Success, viewModel.state.value)

দাবি করার আগে সারিতে থাকা টাস্ক এক্সিকিউট করতে waitForIdle বা runOnIdle ব্যবহার করুন।

বিকল্প ১: UI অ্যাক্টিভ না থাকা পর্যন্ত waitForIdle ব্যবহার করলে ক্লক এগিয়ে যায়, এর ফলে করুটিন রান করেছে কিনা তা যাচাই করা যায়।

viewModel.loadData()

// Explicitly run all queued tasks
composeTestRule.waitForIdle()

assertEquals(Success, viewModel.state.value)

বিকল্প ২: runOnIdle ব্যবহার করলে, UI থ্রেডে কোড ব্লক এক্সিকিউট করা হয়, তবে তা তখনই করা হয় যখন UI আর কোনও কাজ করে না।

viewModel.loadData()

// Run the assertion after the UI is idle
composeTestRule.runOnIdle {
    assertEquals(Success, viewModel.state.value)
}

ম্যানুয়াল সিঙ্ক্রোনাইজেশন

ম্যানুয়াল সিঙ্ক্রোনাইজেশন সংক্রান্ত পরিস্থিতিতে, যেমন অটো-অ্যাডভান্সিং বন্ধ করা থাকলে, করুটিন লঞ্চ করলে তা সঙ্গে সঙ্গে এক্সিকিউট হয় না, কারণ টেস্ট ক্লক পজ করা থাকে। ভার্চুয়াল ঘড়ি না বাড়িয়ে কোয়েরিতে থাকা কোরাউটিন এক্সিকিউট করতে, runCurrent() API ব্যবহার করুন। এটি বর্তমান ভার্চুয়াল সময়ের জন্য শিডিউল করা টাস্ক রান করে।

composeTestRule.mainClock.scheduler.runCurrent()

waitForIdle()-এর বিপরীতে, যা UI স্থিতিশীল না হওয়া পর্যন্ত টেস্ট ক্লক এগিয়ে নিয়ে যায়, runCurrent() বর্তমান ভার্চুয়াল টাইম বজায় রেখে বাকি থাকা টাস্ক এক্সিকিউট করে। এই আচরণ মধ্যবর্তী স্টেটের যাচাইকরণকে সক্ষম করে যা ঘড়িটি কোনও আইডল স্টেটে উন্নত করা হলে অন্যথায় এড়িয়ে যাওয়া হত।

টেস্ট এনভায়রনমেন্টে ব্যবহৃত অন্তর্নিহিত টেস্ট শিডিউলার এক্সপোজ করা হয়। এই শিডিউলারটি Kotlin runTest API-এর সাথে একসাথে ব্যবহার করে টেস্ট ক্লক সিঙ্ক্রোনাইজ করা যেতে পারে।

runComposeUiTest-এ মাইগ্রেট করুন

আপনি Kotlin runTest API-এর পাশাপাশি Compose টেস্ট API ব্যবহার করলে, runComposeUiTest-এ পরিবর্তন করার জন্য বিশেষভাবে সাজেস্ট করা হয়।

আগের পদ্ধতি

runTest-এর সাথে createComposeRule ব্যবহার করলে দুটি আলাদা ঘড়ি তৈরি হয়: একটি Compose-এর জন্য এবং একটি টেস্ট কোরাউটিন স্কোপের জন্য। এই কনফিগারেশনের ফলে আপনাকে টেস্ট শিডিউলার ম্যানুয়ালি সিঙ্ক্রোনাইজ করতে হতে পারে।

@get:Rule
val composeTestRule = createComposeRule()

@Test
fun testWithCoroutines() {
    composeTestRule.setContent {
        var status by remember { mutableStateOf("Loading...") }
        LaunchedEffect(Unit) {
            delay(1000)
            status = "Done!"
        }
        Text(text = status)
    }

    // NOT RECOMMENDED
    // Fails: runTest creates a new, separate scheduler.
    // Advancing time here does NOT advance the compose clock.
    // To fix this without migrating, you would need to share the scheduler
    // by passing 'composeTestRule.mainClock.scheduler' to runTest.
    runTest {
        composeTestRule.onNodeWithText("Loading...").assertIsDisplayed()
        advanceTimeBy(1000)
        composeTestRule.onNodeWithText("Done!").assertIsDisplayed()
    }
}

runComposeUiTest API অটোমেটিক নিজস্ব runTest স্কোপের মধ্যে আপনার টেস্ট ব্লক এক্সিকিউট করে। টেস্ট ক্লকটি কম্পোজ এনভায়রনমেন্টের সাথে সিঙ্ক্রোনাইজ করা হয়, তাই আপনাকে আর ম্যানুয়ালি শিডিউলার ম্যানেজ করতে হবে না।

    @Test
    fun testWithCoroutines() = runComposeUiTest {
        setContent {
            var status by remember { mutableStateOf("Loading...") }
            LaunchedEffect(Unit) {
                delay(1000)
                status = "Done!"
            }
            Text(text = status)
        }

        onNodeWithText("Loading...").assertIsDisplayed()
        mainClock.advanceTimeBy(1000 + 16 /* Frame buffer */)
        onNodeWithText("Done!").assertIsDisplayed()
    }
}