גרסאות 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, כמו בדוגמה הבאה:
val testConfig = ComposeUiTestConfig( effectContext = EmptyCoroutineContext, runTestContext = EmptyCoroutineContext, testTimeout = 30.seconds ) @get:Rule val rule = createComposeRule(config = testConfig) // OR runComposeUiTest(config = testConfig) {}
שיטת הקלט שמוגדרת כברירת מחדל
יכול להיות שהבדיקות ייכשלו במהלך ההעברה אם הן מסתמכות על מצבי קלט שאינם מגע שהוגדרו באמצעות ממשקי 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 לבדיקת כתיבה באמצעות הפרומפט הבא:
הנחיית AI
מעבר מממשקי API לבדיקה בגרסה 1 לממשקי API לבדיקה בגרסה 2
הפרומפט הזה ישתמש במדריך הזה כדי לבצע מיגרציה לגרסה 2 של ממשקי API לבדיקות.
Migrate to Compose testing v2 APIs using the official
migration guide.בטבלה הבאה מפורטים ממשקי API מגרסה 1 שהוצאו משימוש והגרסאות החדשות שלהם (גרסה 2):
הוצא משימוש (גרסה 1) |
החלפה (גרסה 2) |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
תאימות לדורות קודמים ומקרים חריגים
ממשקי ה-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() } }