अपने टेस्ट सिंक करना

Compose टेस्ट, डिफ़ॉल्ट रूप से आपके यूज़र इंटरफ़ेस (यूआई) के साथ सिंक होते हैं. ComposeTestRule के साथ किसी दावे या कार्रवाई को कॉल करने पर, टेस्ट को पहले ही सिंक कर दिया जाता है. इसके बाद, यूज़र इंटरफ़ेस (यूआई) ट्री के खाली होने तक इंतज़ार किया जाता है.

आम तौर पर, आपको कुछ भी करने की ज़रूरत नहीं होती. हालांकि, कुछ खास मामलों के बारे में आपको पता होना चाहिए.

जब किसी टेस्ट को सिंक किया जाता है, तो वर्चुअल क्लॉक का इस्तेमाल करके, Compose ऐप्लिकेशन को समय के हिसाब से आगे बढ़ाया जाता है. इसका मतलब है कि Compose टेस्ट रीयल टाइम में नहीं चलते हैं, ताकि वे जल्द से जल्द पास हो सकें.

हालांकि, अगर आपने टेस्ट को सिंक करने वाले तरीकों का इस्तेमाल नहीं किया, तो कोई रीकंपोज़िशन नहीं होगा और यूज़र इंटरफ़ेस (यूआई) रुका हुआ दिखेगा.

@Test
fun counterTest() {
    val myCounter = mutableStateOf(0) // State that can cause recompositions.
    var lastSeenValue = 0 // Used to track recompositions.
    composeTestRule.setContent {
        Text(myCounter.value.toString())
        lastSeenValue = myCounter.value
    }
    myCounter.value = 1 // The state changes, but there is no recomposition.

    // Fails because nothing triggered a recomposition.
    assertTrue(lastSeenValue == 1)

    // Passes because the assertion triggers recomposition.
    composeTestRule.onNodeWithText("1").assertExists()
}

ध्यान दें कि यह ज़रूरी शर्त सिर्फ़ Compose की हैरारकी पर लागू होती है, ऐप्लिकेशन के बाकी हिस्सों पर नहीं.

अपने-आप सिंक होने की सुविधा बंद करना

जब ComposeTestRule के ज़रिए किसी दावे या कार्रवाई को कॉल किया जाता है, जैसे कि assertExists(), तो आपका टेस्ट, Compose यूज़र इंटरफ़ेस (यूआई) के साथ सिंक हो जाता है. कुछ मामलों में, हो सकता है कि आप इस सिंक्रनाइज़ेशन को रोकना चाहें और घड़ी को खुद कंट्रोल करना चाहें. उदाहरण के लिए, किसी ऐनिमेशन के सटीक स्क्रीनशॉट लेने के लिए, समय को कंट्रोल किया जा सकता है. ऐसा उस समय किया जा सकता है, जब यूज़र इंटरफ़ेस (यूआई) अब भी व्यस्त हो. अपने-आप सिंक होने की सुविधा बंद करने के लिए, mainClock में autoAdvance प्रॉपर्टी को false पर सेट करें:

composeTestRule.mainClock.autoAdvance = false

आम तौर पर, इसके बाद आपको खुद ही समय आगे बढ़ाना होगा. advanceTimeByFrame() का इस्तेमाल करके, ठीक एक फ़्रेम आगे बढ़ा जा सकता है. इसके अलावा, advanceTimeBy() का इस्तेमाल करके, किसी खास अवधि तक आगे बढ़ा जा सकता है:

composeTestRule.mainClock.advanceTimeByFrame()
composeTestRule.mainClock.advanceTimeBy(milliseconds)

इस्तेमाल नहीं किए जा रहे संसाधन

Compose, टेस्ट और यूज़र इंटरफ़ेस (यूआई) को सिंक कर सकता है, ताकि हर कार्रवाई और दावा, आइडल स्टेट में किया जा सके. इसके लिए, ज़रूरत के हिसाब से इंतज़ार किया जा सकता है या घड़ी को आगे बढ़ाया जा सकता है. हालांकि, कुछ एसिंक्रोनस कार्रवाइयां, जिनके नतीजे यूज़र इंटरफ़ेस (यूआई) की स्थिति पर असर डालते हैं, उन्हें बैकग्राउंड में चलाया जा सकता है. ऐसा तब होता है, जब टेस्ट को उनके बारे में पता न हो.

अपने टेस्ट में ये आइडलिंग रिसॉर्स बनाएं और रजिस्टर करें, ताकि टेस्ट किए जा रहे ऐप्लिकेशन के व्यस्त या खाली होने का फ़ैसला लेते समय, इन्हें ध्यान में रखा जा सके. आपको तब तक कोई कार्रवाई करने की ज़रूरत नहीं है, जब तक आपको अतिरिक्त आइडलिंग रिसॉर्स रजिस्टर करने की ज़रूरत न हो. उदाहरण के लिए, अगर आपको कोई ऐसा बैकग्राउंड जॉब चलाना है जो Espresso या Compose के साथ सिंक नहीं किया गया है.

यह एपीआई, Espresso के आइडलिंग रिसॉर्स से काफ़ी मिलता-जुलता है. इससे यह पता चलता है कि टेस्ट किया जा रहा सब्जेक्ट, आइडल है या बिज़ी. IdlingResource को लागू करने के लिए, Compose टेस्ट रूल का इस्तेमाल करें.

composeTestRule.registerIdlingResource(idlingResource)
composeTestRule.unregisterIdlingResource(idlingResource)

मैन्युअल तरीके से सिंक करना

कुछ मामलों में, आपको Compose यूज़र इंटरफ़ेस (यूआई) को अपने टेस्ट के अन्य हिस्सों या टेस्ट किए जा रहे ऐप्लिकेशन के साथ सिंक करना पड़ता है.

waitForIdle() फ़ंक्शन, Compose के इस्तेमाल में न होने का इंतज़ार करता है. हालांकि, यह फ़ंक्शन autoAdvance प्रॉपर्टी पर निर्भर करता है:

composeTestRule.mainClock.autoAdvance = true // Default
composeTestRule.waitForIdle() // Advances the clock until Compose is idle.

composeTestRule.mainClock.autoAdvance = false
composeTestRule.waitForIdle() // Only waits for idling resources to become idle.

ध्यान दें कि दोनों मामलों में, waitForIdle() को बाकी बचे ड्रॉ और लेआउट पास का भी इंतज़ार करना पड़ता है.

इसके अलावा, advanceTimeUntil() का इस्तेमाल करके, घड़ी को तब तक आगे बढ़ाया जा सकता है, जब तक कि कोई शर्त पूरी न हो जाए.

composeTestRule.mainClock.advanceTimeUntil(timeoutMs) { condition }

ध्यान दें कि दी गई शर्त में उस स्थिति की जांच की जानी चाहिए जिस पर इस क्लॉक का असर पड़ सकता है. यह सिर्फ़ Compose की स्थिति के साथ काम करता है.

ऐनिमेशन टेस्ट ऑप्टिमाइज़ करना

हाई-फ़िडेलिटी ऐनिमेशन की जांच करते समय, आपको अक्सर अपने-आप आगे बढ़ने की सुविधा बंद करनी पड़ती है. साथ ही, इंटरमीडिएट यूज़र इंटरफ़ेस (यूआई) की स्थितियों की पुष्टि करने के लिए, फ़्रेम को मैन्युअल तरीके से आगे बढ़ाना पड़ता है. फ़्रेम-दर-फ़्रेम वाले इन खास लूप के लिए, अपने दावे लागू करने के लिए runWithoutImplicitWait तरीके का इस्तेमाल करें. स्टैंडर्ड नोड क्वेरी (जैसे, onNodeWithTag या fetchSemanticsNode) इंप्लिसिट सिंक्रनाइज़ेशन को ट्रिगर करती हैं. ये सिंक्रनाइज़ेशन तब ग़ैर-ज़रूरी होते हैं, जब घड़ी को मैन्युअल तरीके से कंट्रोल किया जा रहा हो. इसलिए, इन्हें बायपास करने से टेस्ट रनटाइम काफ़ी तेज़ हो जाता है.

इस्तेमाल से जुड़े दिशा-निर्देश

  • मैन्युअल क्लॉक मैनेजमेंट: इस एपीआई का इस्तेमाल तब करें, जब mainClock.autoAdvance को false पर सेट किया गया हो और यूज़र इंटरफ़ेस (यूआई) मौजूदा फ़्रेम के लिए, पहले से तय और स्थिर स्थिति में हो.
  • यूज़र इंटरफ़ेस (यूआई) थ्रेड को लागू करना: यूज़र इंटरफ़ेस (यूआई) ट्री की स्थिरता पक्का करने के लिए, यूज़र इंटरफ़ेस (यूआई) थ्रेड पर runWithoutImplicitWait को कॉल करें. जैसे, runOnUiThread के साथ. यूज़र इंटरफ़ेस (यूआई) थ्रेड से बाहर चलाने पर, आपके टेस्ट में रेस कंडिशन और पुरानी स्थिति को पढ़ने की समस्या आ सकती है.
  • रीड-ओनली असर्शन: ब्लॉक में सिर्फ़ रीड-ओनली असर्शन होने चाहिए. स्टेट में बदलाव करने वाली कोई भी कार्रवाई, इस ब्लॉक के बाहर की जानी चाहिए.

उदाहरण

@Test
fun runWithoutImplicitWaitSample() = runComposeUiTest {
    setContent { MainScreen() }
    mainClock.autoAdvance = false

    // Trigger an animation
    onNodeWithText("Start Animation").performClick()

    // Step through the animation frame-by-frame
    while (hasPendingWork()) {
        mainClock.advanceTimeByFrame()
        waitForIdle()
        runOnUiThread {
            // Suppress implicit synchronization inside this block to avoid redundant
            // waits on each node query, making the frame assertions execute much faster.
            runWithoutImplicitWait {
                val box1 = onNodeWithTag("Box1").fetchSemanticsNode()
                val box2 = onNodeWithTag("Box2").fetchSemanticsNode()
                val box3 = onNodeWithTag("Box3").fetchSemanticsNode()

                // Assert the exact intermediate state of all three properties for this frame
                assert(box1.boundsInRoot.right <= box2.boundsInRoot.left)
                assert(box2.boundsInRoot.right <= box3.boundsInRoot.left)
            }
        }
    }
}

मुख्य थ्रेड सिंक्रनाइज़ेशन

Compose टेस्टिंग में अब मुख्य थ्रेड को सिंक करने की सुविधा काम करती है. इससे, सीधे मुख्य थ्रेड से waitForIdle को सुरक्षित तरीके से कॉल किया जा सकता है. साथ ही, Compose यूआई की कार्रवाइयों और दावों को भी कॉल किया जा सकता है.

पहले, Compose टेस्टिंग में दो-थ्रेड वाले मॉडल को सख्ती से लागू किया जाता था: जांच करना बैकग्राउंड टेस्ट थ्रेड पर होता था, जबकि यूज़र इंटरफ़ेस (यूआई) के अपडेट मुख्य थ्रेड पर होते थे. मुख्य थ्रेड (उदाहरण के लिए, runOnUiThread ब्लॉक के अंदर) से waitForIdle या runOnIdle जैसे सिंक्रनाइज़ेशन के तरीकों को कॉल करने पर, IllegalStateException दिखेगा. ऐसा इसलिए होता है, क्योंकि फ़्रेमवर्क ने मुख्य थ्रेड के सिंक्रनाइज़ेशन को रोकने के लिए, थ्रेड की सख्त जांच लागू की है.

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

मुख्य थ्रेड सिंक्रनाइज़ेशन का इस्तेमाल कब करना चाहिए

Compose के टेस्ट के लिए, टेस्ट को बैकग्राउंड थ्रेड पर रखना स्टैंडर्ड तरीका है. हालांकि, कुछ खास मामलों में मुख्य थ्रेड को सिंक्रनाइज़ करना काफ़ी फ़ायदेमंद होता है:

  • जटिल व्यू इंटरऑपरेबिलिटी: Compose और लेगसी Android व्यू, दोनों वाले हाइब्रिड यूज़र इंटरफ़ेस (यूआई) की जांच करते समय, व्यू में बदलाव करने के लिए अक्सर मुख्य थ्रेड पर चलाने की ज़रूरत होती है. अब व्यू के साथ इंटरैक्ट किया जा सकता है और Compose नोड पर क्रम से दावा किया जा सकता है. इसके लिए, थ्रेड के कॉन्टेक्स्ट को लगातार स्विच करने की ज़रूरत नहीं है.
  • सिंक्रोनस स्टेट म्यूटेशन: अगर आपका आर्किटेक्चर, पूरी तरह से मेन-थ्रेड से जुड़े स्टेट होल्डर पर निर्भर करता है, तो अब स्टेट में बदलाव किया जा सकता है. साथ ही, मेन थ्रेड को छोड़े बिना, Compose यूज़र इंटरफ़ेस (यूआई) के सेटल होने का इंतज़ार किया जा सकता है.
  • कस्टम टेस्ट रनर: अगर कस्टम टेस्टिंग इन्फ़्रास्ट्रक्चर बनाया जा रहा है या ऐसे एनवायरमेंट का इस्तेमाल किया जा रहा है जहां टेस्ट रनर, मुख्य थ्रेड पर चलता है, तो Compose टेस्ट अब बैकग्राउंड-थ्रेड डेलिगेशन की ज़रूरत के बिना आसानी से चलते हैं.

उदाहरण

पहले, मुख्य थ्रेड पर सिंक करने की अनुमति नहीं थी. इसलिए, डेवलपर को बैकग्राउंड टेस्ट रनर थ्रेड और यूआई थ्रेड के बीच स्विच करना पड़ता था. इस वजह से, टेस्ट अलग-अलग हो जाते थे:

@Test
fun testBidirectionalInteropUIUpdates_old() {
    val scenario = launchFragmentInContainer<InteropFragment>()
    composeTestRule.waitForIdle()
    scenario.onFragment { fragment ->
        fragment.legacyButton.performClick()
    }
    // Jump to Test Thread to verify state settles inside compose
    composeTestRule.waitForIdle()
    composeTestRule.onNodeWithText("Legacy Clicks: 1").assertIsDisplayed()
    composeTestRule.onNodeWithText("Increment Legacy TextView").performClick()
    composeTestRule.waitForIdle()
    // Jump back to Main Thread to verify target view state settles
    scenario.onFragment { fragment ->
        assert(fragment.legacyTextView.text.toString() == "Compose Clicks: 1")
    }
}

मुख्य थ्रेड सिंक्रनाइज़ेशन की सुविधा चालू होने पर, Compose और व्यू हैरारकी (व्यू और व्यू ग्रुप के लेआउट का क्रम) के लिए, एक ही ब्लॉक में असर्शन लागू किए जा सकते हैं:

@Test
fun testBidirectionalInteropUIUpdates_new() {
    val scenario = launchFragmentInContainer<InteropFragment>()
    composeTestRule.waitForIdle()
    scenario.onFragment { fragment ->
        fragment.legacyButton.performClick()
        composeTestRule.waitForIdle()
        composeTestRule.onNodeWithText("Legacy Clicks: 1").assertIsDisplayed()
        composeTestRule.onNodeWithText("Increment Legacy TextView").performClick()
        composeTestRule.waitForIdle()
        assert(fragment.legacyTextView.text.toString() == "Compose Clicks: 1")
    }
}

शर्तों का इंतज़ार करें

अगर कोई शर्त बाहरी काम पर निर्भर करती है, जैसे कि डेटा लोड करना या Android का मेज़र या ड्रॉ (यानी कि Compose के बाहर मेज़र या ड्रॉ करना), तो उसे waitUntil() जैसे सामान्य कॉन्सेप्ट का इस्तेमाल करना चाहिए:

composeTestRule.waitUntil(timeoutMs) { condition }

इनमें से किसी भी waitUntil हेल्पर का भी इस्तेमाल किया जा सकता है:

composeTestRule.waitUntilAtLeastOneExists(matcher, timeoutMs)

composeTestRule.waitUntilDoesNotExist(matcher, timeoutMs)

composeTestRule.waitUntilExactlyOneExists(matcher, timeoutMs)

composeTestRule.waitUntilNodeCount(matcher, count, timeoutMs)

अतिरिक्त संसाधन

  • Android पर ऐप्लिकेशन की टेस्टिंग: Android टेस्टिंग का मुख्य लैंडिंग पेज, टेस्टिंग की बुनियादी बातों और तकनीकों के बारे में ज़्यादा जानकारी देता है.
  • टेस्टिंग की बुनियादी बातें: Android ऐप्लिकेशन की टेस्टिंग से जुड़े मुख्य कॉन्सेप्ट के बारे में ज़्यादा जानें.
  • लोकल टेस्ट: अपने वर्कस्टेशन पर, कुछ टेस्ट लोकल तौर पर चलाए जा सकते हैं.
  • इंस्ट्रुमेंटेड टेस्ट: इंस्ट्रुमेंटेड टेस्ट चलाना भी एक अच्छा तरीका है. इसका मतलब है कि ये टेस्ट सीधे तौर पर डिवाइस पर चलाए जाते हैं.
  • लगातार इंटिग्रेशन: लगातार इंटिग्रेशन की मदद से, टेस्ट को डिप्लॉयमेंट पाइपलाइन में इंटिग्रेट किया जा सकता है.
  • अलग-अलग स्क्रीन साइज़ पर टेस्ट करें: उपयोगकर्ताओं के पास कई तरह के डिवाइस होते हैं. इसलिए, आपको अलग-अलग स्क्रीन साइज़ पर टेस्ट करना चाहिए.
  • Espresso: Espresso को व्यू-आधारित यूज़र इंटरफ़ेस (यूआई) के लिए डिज़ाइन किया गया है. हालांकि, Espresso की जानकारी, Compose की टेस्टिंग के कुछ पहलुओं के लिए अब भी मददगार हो सकती है.