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

כדי לשפר את האמינות של חבילת הבדיקה, אפשר להתקין דרך למעקב אחרי פעולות ברקע, כמו Espresso Idling Resources. בנוסף, אפשר להחליף מודולים לגרסאות בדיקה שאפשר לשלוח אליהן שאילתות לגבי מצב חוסר פעילות או שמשפרות את הסנכרון, כמו TestDispatcher ל-coroutines או RxIdler ל-RxJava.

דרכים לשיפור היציבות
בדיקות גדולות יכולות לזהות הרבה נסיגות בו-זמנית כי הן בודקות כמה רכיבים של האפליקציה. בדרך כלל הן פועלות במהדמנים או במכשירים, כך שהן בעלות רמת נאמנות גבוהה. בדיקות מקיפה מקצה לקצה מספקות כיסוי מקיף, אבל הן נוטות יותר לכשל מדי פעם.
כדי לצמצם את הבעיות האלה, אפשר לנקוט את הפעולות הבאות:
- הגדרת המכשירים בצורה נכונה
- מניעת בעיות בסנכרון
- הטמעת ניסיונות חוזרים
כדי ליצור בדיקות גדולות באמצעות Compose או Espresso, בדרך כלל מתחילים אחת מהפעילויות ומנווטים כמו משתמש, תוך בדיקה שממשק המשתמש פועל בצורה תקינה באמצעות טענות נכוֹנוּת (assertions) או בדיקות צילומי מסך.
מסגרות אחרות, כמו UI Automator, מאפשרות היקף רחב יותר, כי אפשר לבצע פעולות בממשק המשתמש של המערכת ובאפליקציות אחרות. עם זאת, יכול להיות שבבדיקות של UI Automator תצטרכו לבצע יותר סנכרון ידני, ולכן הן פחות מהימנות.
הגדרת מכשירים
קודם כול, כדי לשפר את האמינות של הבדיקות, צריך לוודא שמערכת ההפעלה של המכשיר לא מפריעה באופן בלתי צפוי לביצוע הבדיקות. לדוגמה, כשתיבת דו-שיח של עדכון מערכת מוצגת מעל אפליקציות אחרות או כשאין מספיק מקום בכונן.
ספקי חוות המכשירים מגדירים את המכשירים והמכונות הווירטואליות שלהם, כך שבדרך כלל לא צריך לבצע שום פעולה. עם זאת, יכול להיות שיש להם הנחיות הגדרה משלהם למקרים מיוחדים.
מכשירים בניהול Gradle
אם אתם מנהלים את הסימולטורים בעצמכם, תוכלו להשתמש במכשירים בניהול Gradle כדי להגדיר את המכשירים שבהם תרצו להריץ את הבדיקות:
android {
testOptions {
managedDevices {
localDevices {
create("pixel2api30") {
// Use device profiles you typically see in Android Studio.
device = "Pixel 2"
// Use only API levels 27 and higher.
apiLevel = 30
// To include Google services, use "google".
systemImageSource = "aosp"
}
}
}
}
}
עם ההגדרה הזו, הפקודה הבאה תיצור קובץ אימג' של אמולטור, תפעיל מכונה, תרוץ את הבדיקות ותשבית אותה.
./gradlew pixel2api30DebugAndroidTest
מכשירים בניהול Gradle מכילים מנגנונים לניסיון חוזר במקרה של ניתוק המכשיר ושיפורים אחרים.
מניעת בעיות בסנכרון
רכיבים שמבצעים פעולות ברקע או פעולות אסינכררוניות עלולים להוביל לכשלים בבדיקות, כי משפט הבדיקה בוצע לפני שממשק המשתמש היה מוכן לכך. ככל שהיקף הבדיקה גדל, כך עולה הסיכוי שהיא תהיה לא יציבה. בעיות הסנכרון האלה הן המקור העיקרי לבעיות לא עקביות, כי מסגרות הבדיקה צריכות להסיק אם פעילות הושלמה או אם צריך להמתין עוד.
פתרונות
אפשר להשתמש במשאבי ההמתנה של Espresso כדי לציין מתי אפליקציה עסוקה, אבל קשה לעקוב אחרי כל פעולה אסינכררונית, במיוחד בבדיקות מקצה לקצה גדולות מאוד. בנוסף, יכול להיות שיהיה קשה להתקין משאבים ללא שימוש בלי לזהם את הקוד שנבדק.
במקום להעריך אם פעילות מסוימת עמוסה או לא, אפשר להגדיר שהבדיקות ימתינו עד לעמידה בתנאים ספציפיים. לדוגמה, אפשר להמתין עד שטקסט או רכיב ספציפיים יוצגו בממשק המשתמש.

ל-Compose יש אוסף של ממשקי API לבדיקה כחלק מ-ComposeTestRule
כדי להמתין למתאמים שונים:
fun waitUntilAtLeastOneExists(matcher: SemanticsMatcher, timeout: Long = 1000L)
fun waitUntilDoesNotExist(matcher: SemanticsMatcher, timeout: Long = 1000L)
fun waitUntilExactlyOneExists(matcher: SemanticsMatcher, timeout: Long = 1000L)
fun waitUntilNodeCount(matcher: SemanticsMatcher, count: Int, timeout: Long = 1000L)
ו-API כללי שמקבל כל פונקציה שמחזירה ערך בוליאני:
fun waitUntil(timeoutMillis: Long, condition: () -> Boolean): Unit
דוגמה לשימוש:
composeTestRule.waitUntilExactlyOneExists(hasText("Continue")</code>)</p></td>
מנגנונים לניסיון חוזר
צריך לתקן בדיקות לא יציבות, אבל לפעמים התנאים שגורמים להן להיכשל כה לא סבירים שקשה לשחזר אותם. תמיד צריך לעקוב אחרי בדיקות לא יציבות ולתקן אותן, אבל מנגנון של ניסיונות חוזרים יכול לעזור לשמור על הפרודוקטיביות של המפתחים על ידי הרצת הבדיקה כמה פעמים עד שהיא עוברת.
כדי למנוע בעיות, צריך לבצע ניסיונות חוזרים בכמה רמות, למשל:
- פג הזמן הקצוב לחיבור למכשיר או שהחיבור אבד
- כשל בבדיקה אחת
התקנה או הגדרה של ניסיונות חוזרים תלויה במסגרות הבדיקה ובתשתית שלכם, אבל המנגנונים הנפוצים כוללים:
- כלל JUnit שמנסה שוב כל בדיקה מספר פעמים
- פעולה או שלב של ניסיון חוזר בתהליך העבודה ב-CI
- מערכת להפעלה מחדש של אמולטור כשהוא לא מגיב, למשל במכשירים בניהול Gradle.