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

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

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

ממשקי ה-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 הוא חלק מגרסה 2 של ממשקי ה-API לבדיקות של Compose, שמשתמשים ב-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
    }
}

במאמר כשלים נפוצים ואיך לתקן אותם מפורטות בעיות אחרות בהעברה ופתרונות להן.

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

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

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

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

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

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)

Replacement (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

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

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

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

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

השינוי העיקרי: השפעה על הפעלת קורוטינה

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

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

כשלים נפוצים ואיך לפתור את הבעיה

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

  • כשל: אתם מפעילים משימה (לדוגמה, ViewModel טוען נתונים), אבל טענת הנכוֹנוּת (assertion) נכשלת מיד כי הנתונים עדיין במצב "בטעינה".
  • הסיבה: בממשקי ה-API בגרסה 2, פונקציות coroutine נכנסות לתור במקום לפעול באופן מיידי. המשימה נוספה לתור אבל לא הופעלה בפועל לפני שהתוצאה נבדקה.
  • הפתרון: מקדמים את הזמן באופן מפורש. צריך להגדיר במפורש למנהל התהליכים של גרסה 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 כדי לבצע משימות בתור לפני ההצהרה.

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

viewModel.loadData()

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

assertEquals(Success, viewModel.state.value)

אפשרות 2: שימוש ב-runOnIdle מריץ את בלוק הקוד בשרשור ה-UI אחרי שה-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() מבצע משימות בהמתנה תוך שמירה על הזמן הווירטואלי הנוכחי. ההתנהגות הזו מאפשרת לאמת מצבי ביניים שאחרת היו מדלגים עליהם אם השעון היה מועבר למצב של חוסר פעילות.

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

מעבר אל runComposeUiTest

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

הגישה הקודמת

שימוש ב-createComposeRule בשילוב עם runTest יוצר שני שעונים נפרדים: אחד ל-Compose ואחד להיקף של פונקציית ה-Coroutine לבדיקה. ההגדרה הזו יכולה לאלץ אתכם לסנכרן באופן ידני את מתזמן הבדיקות.

@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. השעון לבדיקה מסונכרן עם סביבת Compose, כך שאין יותר צורך לנהל את המתזמן באופן ידני.

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