סנכרון הבדיקות

כברירת מחדל, בדיקות של פיתוח נייטיב מסונכרנות עם ממשק המשתמש. כשמפעילים טענת נכוֹנוּת (assertion) או פעולה באמצעות 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()
}

חשוב לציין שהדרישה הזו חלה רק על היררכיות של פיתוח נייטיב, ולא על שאר חלקי האפליקציה.

השבתת הסנכרון האוטומטי

כשמפעילים טענת נכוֹנוּת (assertion) או פעולה דרך ComposeTestRule, כמו assertExists(), הבדיקה מסונכרנת עם ממשק המשתמש של פיתוח נייטיב. במקרים מסוימים, יכול להיות שתרצו להפסיק את הסנכרון הזה ולשלוט בשעון בעצמכם. לדוגמה, אתם יכולים לשלוט בזמן כדי לצלם צילומי מסך מדויקים של אנימציה בנקודה שבה ממשק המשתמש עדיין יהיה עסוק. כדי להשבית את הסנכרון האוטומטי, מגדירים את המאפיין autoAdvance ב-mainClock לערך false:

composeTestRule.mainClock.autoAdvance = false

בדרך כלל, אתם תצטרכו להעביר את הזמן בעצמכם. אפשר להתקדם בדיוק פריים אחד באמצעות advanceTimeByFrame() או למשך זמן ספציפי באמצעות advanceTimeBy():

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

משאבים לא פעילים

‫Compose יכול לסנכרן את הבדיקות ואת ממשק המשתמש, כך שכל פעולה וטענת נכוֹנוּת (assertion) מתבצעות במצב של חוסר פעילות, בהמתנה או בהתקדמות של השעון לפי הצורך. עם זאת, יכול להיות שפעולות אסינכרוניות מסוימות שהתוצאות שלהן משפיעות על מצב ממשק המשתמש יופעלו ברקע בלי שהבדיקה תהיה מודעת להן.

צריך ליצור ולרשום את המשאבים האלה בהמתנה לפעילות בבדיקה, כדי שהם יילקחו בחשבון כשמחליטים אם האפליקציה שנבדקת עסוקה או לא פעילה. לא צריך לעשות כלום, אלא אם אתם צריכים לרשום משאבים נוספים בהמתנה לפעילות. לדוגמה, אם אתם מריצים משימה ברקע שלא מסונכרנת עם Espresso או Compose.

ה-API הזה דומה מאוד ל-Idling Resources של Espresso, והוא מציין אם הנושא שנבדק נמצא במצב פעיל או לא פעיל. משתמשים בכלל הבדיקה של Compose כדי לרשום את ההטמעה של IdlingResource.

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).

בדיקות אופטימיזציה של אנימציה

כשבודקים אנימציות באיכות גבוהה, בדרך כלל צריך להשבית את המעבר האוטומטי ולעבור ידנית בין הפריימים כדי לאשר מצבי ביניים של ממשק המשתמש. בלולאות הספציפיות האלה, שפועלות פריים אחרי פריים, צריך להשתמש ב-method‏ runWithoutImplicitWait כדי להריץ את טענות הנכוֹנוּת (assertions). שאילתות צומת רגילות (כמו onNodeWithTag או fetchSemanticsNode) מפעילות סנכרונים משתמעים שהם מיותרים כששולטים בשעון באופן ידני, כך שמעקף שלהם מאיץ באופן משמעותי את זמני הריצה של הבדיקה.

הנחיות שימוש

  • ניהול ידני של השעון: משתמשים ב-API הזה כש-mainClock.autoAdvance מוגדר ל-false וממשק המשתמש נמצא במצב יציב וידוע בפריים הנוכחי.
  • הפעלה של שרשור UI: כדי לוודא שעץ ממשק המשתמש יציב, צריך להפעיל את runWithoutImplicitWait בשרשור UI, למשל באמצעות runOnUiThread. אם מריצים אותה מחוץ לשרשור ה-UI, הבדיקה חשופה למרוצי תהליכים ולקריאות של מצבים לא עדכניים.
  • טענות נכונות (assertions) לקריאה בלבד: הבלוק צריך להכיל רק טענות נכונות לקריאה בלבד. כל פעולה שמשנה את המצב צריכה להתבצע מחוץ לבלוק הזה.

דוגמה

@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)
            }
        }
    }
}

סנכרון ה-thread הראשי

עכשיו אפשר לבצע בדיקות ב-Compose עם סנכרון של ה-thread הראשי, וכך להפעיל בבטחה את waitForIdle – וגם פעולות וטענות נכונות (assertions) של ממשק המשתמש של Compose – ישירות מה-thread הראשי.

בעבר, בדיקות של Compose אכפו באופן קפדני מודל של שני שרשורים: ביצוע הבדיקה התרחש בשרשור בדיקה ברקע, ועדכוני ממשק המשתמש התרחשו בשרשור הראשי. קריאה לשיטות סנכרון כמו waitForIdle או runOnIdle מה-thread הראשי (לדוגמה, בתוך בלוק runOnUiThread) תגרום להצגת IllegalStateException כי המסגרת אוכפת בדיקות מחמירות של ה-thread כדי למנוע סנכרון של ה-thread הראשי.

כשהסנכרון של ה-thread הראשי מופעל, מסגרת הבדיקה של Compose יכולה לקדם את השעון ולעבד עבודה בהמתנה גם כשמתבצעות קריאות חסימה ב-thread הראשי.

מתי כדאי להשתמש בסנכרון של השרשור הראשי

למרות שהרצת בדיקות ב-thread ברקע היא עדיין הסטנדרט לבדיקות של פיתוח נייטיב בלבד, לסנכרון של ה-thread הראשי יש יתרונות משמעותיים בכמה תרחישים ספציפיים:

  • יכולת פעולה הדדית מורכבת של תצוגות (Views): כשבודקים ממשקי משתמש היברידיים שמכילים גם פיתוח נייטיב וגם תצוגות Android מדור קודם, כדי לשנות את התצוגות בדרך כלל צריך להפעיל את ה-thread הראשי. עכשיו אתם יכולים לבצע אינטראקציה עם תצוגות ולקבוע טענות נכונות (assert) בצומתי Compose ברצף, בלי להחליף כל הזמן את ההקשרים של השרשורים.
  • שינויים סינכרוניים במצב: אם הארכיטקטורה שלכם מסתמכת על מאחסני מצב שקשורים אך ורק ל-main thread, עכשיו אתם יכולים לשנות את המצב ולהמתין מיד להתייצבות של ממשק המשתמש של Compose בלי לצאת מה-main thread.
  • מריצי בדיקות בהתאמה אישית: אם אתם מפתחים תשתית בדיקה בהתאמה אישית או משתמשים בסביבות שבהן מריץ הבדיקות פועל באופן טבעי ב-thread הראשי, בדיקות Compose פועלות עכשיו בצורה חלקה בלי צורך בהקצאה ל-thread ברקע.

דוגמה

בעבר, בגלל שהיה אסור לבצע סנכרון בשרשור ה-UI, המפתחים היו צריכים לעבור הלוך ושוב בין שרשור ה-test runner ברקע לבין שרשור ה-UI, מה שהוביל לבדיקות לא רציפות:

@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")
    }
}

כשהסנכרון של ה-thread הראשי מופעל, אפשר להריץ את טענות הנכוֹנוּת (assertions) של היררכיות Compose ו-View באותו בלוק:

@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.
  • בדיקות מקומיות: אתם יכולים להריץ בדיקות מסוימות באופן מקומי, בתחנת העבודה שלכם.
  • בדיקות אינסטרומנטציה: מומלץ להריץ גם בדיקות אינסטרומנטציה. כלומר, בדיקות שפועלות ישירות במכשיר.
  • אינטגרציה רציפה (CI): אינטגרציה רציפה מאפשרת לכם לשלב את הבדיקות בצינור עיבוד הנתונים לפריסה.
  • בדיקה של גדלי מסך שונים: יש כל כך הרבה מכשירים זמינים למשתמשים, לכן כדאי לבדוק גדלי מסך שונים.
  • ‫Espresso: למרות שהכלי הזה מיועד לממשקי משתמש שמבוססים על תצוגות, הידע ב-Espresso עדיין יכול לעזור בהיבטים מסוימים של בדיקות ב-Compose.