כברירת מחדל, בדיקות Compose מסונכרנות עם ממשק המשתמש. כשקוראים לאסרטיב או לפעולה באמצעות ComposeTestRule, הבדיקה מסונכרנת מראש, וממתינה עד שעץ ממשק המשתמש לא פעיל.
בדרך כלל, לא צריך לעשות שום דבר. עם זאת, יש כמה מקרי קצה שחשוב להכיר.
כשמבצעים סנכרון של בדיקה, האפליקציה שלכם ל-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. במקרים מסוימים, יכול להיות שתרצו להפסיק את הסנכרון הזה ולשלוט בשעון בעצמכם. לדוגמה, אתם יכולים לשלוט בזמן כדי לצלם צילומי מסך מדויקים של אנימציה בנקודה שבה ממשק המשתמש עדיין יהיה עסוק. כדי להשבית את הסנכרון האוטומטי, צריך להגדיר את המאפיין autoAdvance ב-mainClock לערך false:
composeTestRule.mainClock.autoAdvance = false
בדרך כלל, בשלב הזה אתם מקדמים את השעה בעצמכם. אפשר להתקדם בדיוק פריים אחד באמצעות advanceTimeByFrame() או להתקדם למשך זמן ספציפי באמצעות advanceTimeBy():
composeTestRule.mainClock.advanceTimeByFrame()
composeTestRule.mainClock.advanceTimeBy(milliseconds)
משאבים בהמתנה לפעילות
ב-Compose אפשר לסנכרן בין הבדיקות לבין ממשק המשתמש, כך שכל פעולה וכל טענה מתבצעות במצב המתנה, תוך המתנה או קידום של השעון לפי הצורך. עם זאת, אפשר להריץ ברקע פעולות אסינכרוניות מסוימות שהתוצאות שלהן משפיעות על מצב ממשק המשתמש, בלי שהבדיקה תהיה מודעת להן.
כדאי ליצור ולרשום את משאבי ההמתנה האלה בבדיקה, כדי שהם ייכללו בחישובים שקובעים אם האפליקציה שנבדקת עסוקה או במצב המתנה. אין צורך לבצע פעולה כלשהי אלא אם אתם צריכים לרשום משאבים נוספים של מצב המתנה, למשל אם אתם מריצים משימה ברקע שלא מסונכרנת עם 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).
אופטימיזציה של בדיקות אנימציה
测试高保真动画时,您通常需要停用自动前进功能,并手动逐帧浏览,以断言中间界面状态。对于这些特定的逐帧循环,请使用 runWithoutImplicitWait 方法来执行断言。当您手动控制时钟时,标准节点查询(例如 onNodeWithTag 或 fetchSemanticsNode)会触发冗余的隐式同步,因此绕过这些查询可以显著缩短测试运行时长。
使用指南
- 手动时钟管理:当
mainClock.autoAdvance设置为false且界面处于当前帧的已知稳定状态时,请使用此 API。 - 界面线程执行:为确保界面树的稳定性,请在界面线程上调用
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) } } } }
סנכרון ה-thread הראשי
בדיקות Compose תומכות עכשיו בסנכרון של ה-thread הראשי, כך שאפשר לקרוא בבטחה ל-waitForIdle – ובהרחבה, לפעולות ולאישורים של ממשק המשתמש של Compose – ישירות מה-thread הראשי.
בעבר, בדיקות Compose אכפו באופן קפדני מודל של שני שרשורים: ביצוע הבדיקה התרחש בשרשור בדיקה ברקע, בעוד שעדכוני ממשק המשתמש התרחשו בשרשור הראשי. קריאה לשיטות סנכרון כמו waitForIdle או runOnIdle
מה-thread הראשי (לדוגמה, בתוך בלוק runOnUiThread) תגרום לשגיאת IllegalStateException כי המסגרת אוכפת בדיקות thread קפדניות כדי למנוע סנכרון של ה-thread הראשי.
כשסנכרון ה-thread הראשי מופעל, אפשר להשתמש ב-Compose test framework כדי להריץ את השעון ולעבד עבודה בהמתנה, גם כשמתבצעות קריאות חסימה ב-thread הראשי.
מתי כדאי להשתמש בסנכרון של השרשור הראשי
אומנם שמירה של בדיקות ב-thread ברקע היא הסטנדרט לבדיקות Compose טהורות, אבל סנכרון של ה-thread הראשי מועיל מאוד בכמה תרחישים ספציפיים:
- יכולת פעולה הדדית מורכבת של View: כשבודקים ממשקי משתמש היברידיים שמכילים גם פיתוח נייטיב וגם תצוגות (Views) מדור קודם של Android, לעיתים קרובות נדרשת הפעלה בשרשור הראשי כדי לבצע מניפולציה של תצוגות. עכשיו אפשר לקיים אינטראקציה עם תצוגות ולהצהיר על צמתי Compose ברצף בלי לעבור כל הזמן בין הקשרים של השרשורים.
- שינויים סינכרוניים במצב: אם הארכיטקטורה שלכם מסתמכת על מחזיקי מצב שקשורים באופן הדוק לשרשור הראשי, עכשיו אתם יכולים לשנות את המצב ולהמתין מיד עד שממשק המשתמש של Compose יתייצב, בלי לצאת מהשרשור הראשי.
- מריצי בדיקות בהתאמה אישית: אם אתם יוצרים תשתית בדיקות בהתאמה אישית או משתמשים בסביבות שבהן מריץ הבדיקות מופעל באופן מובנה בשרשור הראשי, בדיקות Compose מופעלות עכשיו בצורה נקייה בלי לדרוש העברה לשרשור ברקע.
דוגמה
בעבר, מכיוון שהיה אסור לבצע סנכרון ב-thread הראשי, מפתחים נאלצו לעבור הלוך ושוב בין ה-thread של כלי ההרצה של בדיקות ברקע לבין שרשור 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 הראשי, אפשר להריץ את הטענות לגבי היררכיות של 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.