יציבות של בדיקות גדולות

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

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

בדיקת הסנכרון

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

תרשים זרימה שבו מוצגת לולאה שבה בודקים אם האפליקציה לא פעילה לפני שמבצעים בדיקה
איור 1: בדיקת הסנכרון.

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

תרשים שמציג כשל בבדיקה כשהסנכרון מבוסס על המתנה לזמן קבוע
איור 2: השימוש במצבי שינה בבדיקות מוביל לבדיקות איטיות או רעות.

דרכים לשיפור היציבות

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

כדי לצמצם את הבעיות האלה, אפשר לנקוט את הפעולות הבאות:

  • הגדרת המכשירים בצורה נכונה
  • מניעת בעיות בסנכרון
  • הטמעת ניסיונות חוזרים

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

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

פיתוח נייטיב כולל אוסף של ממשקי 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.