הגדרת סביבת הבדיקה באמצעות ממשקי API לבדיקה בגרסה 2

גרסאות v2 של ממשקי ה-API לבדיקת Compose (‏createComposeRule,‏ createAndroidComposeRule,‏ runComposeUiTest,‏ runAndroidComposeUiTest וכו') זמינות עכשיו כדי לשפר את השליטה בהרצת קורוטינות. העדכון הזה לא משכפל את כל ממשקי ה-API, אלא רק את אלה שיוצרים את סביבת הבדיקה.

הוצאנו משימוש את ממשקי API מגרסה 1, ומומלץ מאוד לעבור לממשקי API החדשים. המיגרציה מאפשרת לוודא שהבדיקות שלכם תואמות להתנהגות הרגילה של קורוטינות, ומונעת בעיות תאימות בעתיד. רשימה של ממשקי API מגרסה 1 שהוצאו משימוש זמינה במאמר מיפוי ממשקי API.

בעוד שממשקי API בגרסה 1 הסתמכו על UnconfinedTestDispatcher, ממשקי API בגרסה 2 משתמשים ב-StandardTestDispatcher כברירת מחדל להרצת ההרכבה. השינוי הזה מתאים את התנהגות הבדיקה של Compose לממשקי ה-API הרגילים של runTest ומספק שליטה מפורשת בסדר ההרצה של שגרות המשך (coroutine).

הגדרת סביבת הבדיקה

ממשקי ה-API של Compose test v2 משתמשים ב-ComposeUiTestConfig כדי להתאים אישית את סביבת הבדיקה. ממשקי API שיוצרים פונקציות הגדרה לבדיקות, כמו createComposeRule,‏ runComposeUiTest וממשקי API קשורים אחרים, מקבלים ComposeUiTestConfig. אובייקט ההגדרה הזה מאחד ממשקי API שקשורים לסביבה, כמו effectContext,‏ runTestContext ו-testTimeout, לאובייקט אחד.

מודל ההגדרות גם מנהל את inputMode. ממשקי API של Compose test v2 אוכפים InputMode.Touch כברירת מחדל בתחילת כל בדיקה כדי להבטיח דטרמיניזם ולמנוע מצב של דליפת מצב של מצב קלט בין בדיקות.

ComposeUiTestConfig הוא חלק מממשקי ה-API של בדיקות Compose גרסה 2, שמשתמשים ב-StandardTestDispatcher כברירת מחדל. אם הבדיקות שלכם משתמשות בממשקי API מגרסה 1, כדאי לעיין במאמר מעבר לממשקי API לבדיקות מגרסה 2 לפני שמשתמשים ב-ComposeUiTestConfig.

העברה אל 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
    }
}

לפתרון בעיות אחרות בהעברה, אפשר לעיין במאמר בעיות נפוצות ואיך לפתור אותן.

מעבר לגרסה 2 של ממשקי ה-API לבדיקות

כשמשדרגים ל-API מגרסה 2, בדרך כלל אפשר להשתמש בחיפוש והחלפה כדי לעדכן את ייבוא החבילות ולאמץ את השינויים החדשים ב-dispatcher.

לחלופין, אפשר לבקש מ-Gemini לבצע העברה לגרסה 2 של ממשקי ה-API לבדיקת כתיבה באמצעות הפרומפט הבא:

מעבר מממשקי API לבדיקה בגרסה 1 לממשקי API לבדיקה בגרסה 2

הפרומפט הזה ישתמש במדריך הזה כדי לבצע מיגרציה לגרסה 2 של ממשקי API לבדיקות.

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

שימוש בפרומפטים ל-AI

הנחיות ל-AI מיועדות לשימוש ב-Gemini ב-Android Studio.

מידע נוסף על Gemini ב-Studio זמין כאן: https://developer.android.com/studio/gemini/overview

בטבלה הבאה מפורטים ממשקי API מגרסה 1 שהוצאו משימוש והגרסאות החדשות שלהם (גרסה 2):

הוצא משימוש (גרסה 1)

החלפה (גרסה 2)

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

תאימות לדורות קודמים ומקרים חריגים

ממשקי ה-API הקיימים של גרסה 1 הוצאו משימוש, אבל הם ממשיכים להשתמש ב-UnconfinedTestDispatcher כדי לשמור על ההתנהגות הקיימת ולמנוע שינויים שעלולים לשבור את התאימות.

החריג היחיד שבו התנהגות ברירת המחדל השתנתה הוא:

השולח של בדיקות ברירת המחדל שמשמש להרצת קומפוזיציה במחלקה AndroidComposeUiTestEnvironment השתנה מ-UnconfinedTestDispatcher ל-StandardTestDispatcher. הבעיה הזו משפיעה על מקרים שבהם יוצרים מופע באמצעות בנאי, או תת-מחלקה AndroidComposeUiTestEnvironment, ומפעילים את הבנאי הזה.

שינוי מרכזי: השפעה על הרצה של שגרות המשך (coroutine)

ההבדל העיקרי בין גרסה 1 לגרסה 2 של ממשקי ה-API הוא האופן שבו מתבצעת ההפצה של קורוטינות:

  • v1 APIs ‏ (UnconfinedTestDispatcher): כשקורוטינה הופעלה, היא בוצעה באופן מיידי בשרשור הנוכחי, ולעתים קרובות הסתיימה לפני שהופעלה השורה הבאה של קוד הבדיקה. בניגוד להתנהגות בסביבת הייצור, ההפעלה המיידית הזו עלולה להסתיר בטעות בעיות תזמון אמיתיות או תנאי מירוץ שיתרחשו באפליקציה פעילה.
  • v2 APIs ‏(StandardTestDispatcher): כשמפעילים שגרת המשך (coroutine), היא מתווספת לתור ולא מופעלת עד שהבדיקה מקדמת באופן מפורש את השעון הווירטואלי. ממשקי API סטנדרטיים של Compose לבדיקות (כמו waitForIdle()) כבר מטפלים בסנכרון הזה, ולכן רוב הבדיקות שמסתמכות על ממשקי ה-API הסטנדרטיים האלה ימשיכו לפעול בלי שינויים.

בעיות נפוצות ופתרונות

אם הבדיקות נכשלות אחרי שמשדרגים לגרסה 2, סביר להניח שהן יציגו את הדפוס הבא:

  • כשל: אתם מפעילים משימה (לדוגמה, ViewModel טוען נתונים), אבל הטענה נכשלת מיד כי הנתונים עדיין במצב 'טעינה'.
  • הסיבה: בממשקי API מגרסה 2, קורוטינות מתווספות לתור במקום להיות מופעלות באופן מיידי. המשימה הוכנסה לתור אבל לא הורצה לפני שהתוצאה נבדקה.
  • תיקון: מקדמים את הזמן באופן מפורש. צריך לציין במפורש למרכז הבקשות מגרסה 2 מתי לבצע את העבודה.

הגישה הקודמת

בגרסה 1, המשימה הופעלה והסתיימה מיד. בגרסה 2, הקוד הבא נכשל כי 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 כדי להריץ משימות בתור לפני שמשתמשים ב-assert.

אפשרות 1: שימוש ב-waitForIdle מקדם את השעון עד שממשק המשתמש בלי פעילות, וכך מוודאים ששגרת ההמשך (coroutine) פעלה.

viewModel.loadData()

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

assertEquals(Success, viewModel.state.value)

אפשרות 2: שימוש ב-runOnIdle מפעיל את בלוק הקוד בשרשור UI אחרי שממשק המשתמש בלי פעילות.

viewModel.loadData()

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

סנכרון ידני

בתרחישים שכוללים סנכרון ידני, כמו כשמשביתים את ההתקדמות האוטומטית, הפעלה של שגרת המשך (coroutine) לא מובילה להרצה מיידית כי שעון הבדיקה מושהה. כדי להריץ קורוטינות בתור בלי להקדים את השעון הווירטואלי, משתמשים בממשק ה-API‏ runCurrent(). הפקודה הזו מריצה משימות שמתוזמנות לשעה הוירטואלית הנוכחית.

composeTestRule.mainClock.scheduler.runCurrent()

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

מערכת התזמון הבסיסית של הבדיקות שמשמשת בסביבת הבדיקה נחשפת. אפשר להשתמש במתזמן הזה בשילוב עם Kotlin runTest API כדי לסנכרן את שעון הבדיקה.

העברה אל runComposeUiTest

אם אתם משתמשים בממשקי API של בדיקות Compose לצד Kotlin runTest API, מומלץ מאוד לעבור אל runComposeUiTest.

הגישה הקודמת

השימוש ב-createComposeRule יחד עם runTest יוצר שני שעונים נפרדים: אחד ל-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()
    }
}

ממשק ה-API‏ runComposeUiTest מריץ אוטומטית את בלוק הבדיקה בהיקף 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()
    }
}