כתיבת בדיקות אוטומטיות באמצעות UI Automator

מסגרת הבדיקה UI Automator מספקת קבוצה של ממשקי API לבניית בדיקות ממשק משתמש שפועלות באינטראקציה עם אפליקציות משתמש ואפליקציות מערכת.

מבוא לבדיקות מודרניות של UI Automator

בגרסה 2.4 של UI Automator, הוספנו שפה ספציפית לדומיין (DSL) שמתאימה ל-Kotlin, כדי לפשט את כתיבת בדיקות ממשק משתמש ל-Android. ממשק ה-API החדש הזה מתמקד באיתור אלמנטים שמבוסס על פרדיקטים ובשליטה מפורשת במצבי האפליקציה. אפשר להשתמש בה כדי ליצור בדיקות אוטומטיות שקל יותר לתחזק ושהן אמינות יותר.

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

התכונות העיקריות של הגישה המודרנית כוללות:

  • uiAutomator טווח בדיקה ייעודי לקוד בדיקה נקי וברור יותר.
  • שיטות כמו onElement,‏ onElements ו-onElementOrNull למציאת רכיבי ממשק משתמש עם פרדיקטים ברורים.
  • מנגנון המתנה מובנה לרכיבים מותנים onElement*(timeoutMs: Long = 10000)
  • ניהול מפורש של מצב האפליקציה, כמו waitForStable ו-waitForAppToBeVisible.
  • אינטראקציה ישירה עם צמתי חלונות של נגישות לתרחישי בדיקה של ריבוי חלונות.
  • יכולות מובנות לצילומי מסך ו-ResultsReporter לבדיקות ויזואליות ולניפוי באגים.

הגדרת הפרויקט

כדי להתחיל להשתמש בממשקי ה-API המודרניים של UI Automator, צריך לעדכן את קובץ build.gradle.kts של הפרויקט כך שיכלול את התלות האחרונה:

Kotlin

dependencies {
  ...
  androidTestImplementation("androidx.test.uiautomator:uiautomator:2.4.0-alpha05")
}

מגניב

dependencies {
  ...
  androidTestImplementation "androidx.test.uiautomator:uiautomator:2.4.0-alpha05"
}

מושגים מרכזיים ב-API

בקטעים הבאים מתוארים מושגי ליבה של ממשק ה-API המודרני של UI Automator.

היקף הבדיקה של uiAutomator

גישה לכל ממשקי ה-API החדשים של UI Automator בתוך הבלוק uiAutomator { ... }. הפונקציה הזו יוצרת UiAutomatorTestScope שמספקת סביבה תמציתית ובטוחה מבחינת סוגים לפעולות הבדיקה.

uiAutomator {
  // All your UI Automator actions go here
  startApp("com.example.targetapp")
  onElement { textAsString() == "Hello, World!" }.click()
}

חיפוש רכיבים בממשק המשתמש

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

  • onElement { predicate }: מחזירה את הרכיב הראשון בממשק המשתמש שתואם לפרדיקט בתוך זמן קצוב לתפוגה שמוגדר כברירת מחדל. הפונקציה מחזירה חריגה אם היא לא מוצאת אלמנט תואם.

    // Find a button with the text "Submit" and click it
    onElement { textAsString() == "Submit" }.click()
    
    // Find a UI element by its resource ID
    onElement { viewIdResourceName == "my_button_id" }.click()
    
    // Allow a permission request
    watchFor(PermissionDialog) {
      clickAllow()
    }
    
  • onElementOrNull { predicate }: דומה ל-onElement, אבל מחזירה null אם הפונקציה לא מוצאת רכיב תואם בתוך הזמן הקצוב לתפוגה. לא מופעלת חריגה. משתמשים בשיטה הזו לרכיבים אופציונליים.

    val optionalButton = onElementOrNull { textAsString() == "Skip" }
    optionalButton?.click() // Click only if the button exists
    
  • onElements { predicate }: ממתין עד שלפחות רכיב אחד בממשק המשתמש תואם לפרדיקט שצוין, ואז מחזיר רשימה של כל הרכיבים בממשק המשתמש שתואמים לפרדיקט.

    // Get all items in a list Ui element
    val listItems = onElements { className == "android.widget.TextView" && isClickable }
    listItems.forEach { it.click() }
    

ריכזנו כאן כמה טיפים לשימוש בשיחות onElement:

  • שרשור קריאות onElement לרכיבים מקוננים: אפשר לשרשר קריאות onElement כדי למצוא רכיבים בתוך רכיבים אחרים, לפי היררכיית הורה-צאצא.

    // Find a parent Ui element with ID "first", then its child with ID "second",
    // then its grandchild with ID "third", and click it.
    onElement { viewIdResourceName == "first" }
      .onElement { viewIdResourceName == "second" }
      .onElement { viewIdResourceName == "third" }
      .click()
    
  • כדי להגדיר זמן קצוב לתפוגה לפונקציות onElement*, מעבירים ערך שמייצג אלפיות שנייה.

    // Find a Ui element with a zero timeout (instant check)
    onElement(0) { viewIdResourceName == "something" }.click()
    
    // Find a Ui element with a custom timeout of 10 seconds
    onElement(10_000) { textAsString() == "Long loading text" }.click()
    

אינטראקציה עם רכיבים בממשק המשתמש

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

// Click a Ui element
onElement { textAsString() == "Tap Me" }.click()

// Set text in an editable field
onElement { className == "android.widget.EditText" }.setText("My input text")

// Perform a long click
onElement { contentDescription == "Context Menu" }.longClick()

טיפול במצבי אפליקציה וב-watchers

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

ניהול מחזור החיים של אפליקציות

ממשקי ה-API מספקים דרכים לשלוט במצב האפליקציה שנבדקת:

// Start a specific app by package name. Used for benchmarking and other
// self-instrumenting tests.
startApp("com.example.targetapp")

// Start a specific activity within the target app
startActivity(SomeActivity::class.java)

// Start an intent
startIntent(myIntent)

// Clear the app's data (resets it to a fresh state)
clearAppData("com.example.targetapp")

טיפול בממשק משתמש לא צפוי

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

import androidx.test.uiautomator.PermissionDialog

@Test
fun myTestWithPermissionHandling() = uiAutomator {
  startActivity(MainActivity::class.java)

  // Register a watcher to click "Allow" if a permission dialog appears
  watchFor(PermissionDialog) { clickAllow() }

  // Your test steps that might trigger a permission dialog
  onElement { textAsString() == "Request Permissions" }.click()

  // Example: You can register a different watcher later if needed
  clearAppData("com.example.targetapp")

  // Now deny permissions
  startApp("com.example.targetapp")
  watchFor(PermissionDialog) { clickDeny() }
  onElement { textAsString() == "Request Permissions" }.click()
}

PermissionDialog היא דוגמה ל-ScopedWatcher<T>, כאשר T הוא האובייקט שמועבר כהיקף לבלוק ב-watchFor. אתם יכולים ליצור צופים מותאמים אישית על סמך התבנית הזו.

המתנה עד שהאפליקציה תהיה גלויה ויציבה

לפעמים צריך להמתין עד שהרכיבים יהיו גלויים או יציבים. כדי לעזור לכם בכך, UI Automator מציע כמה ממשקי API.

הפעולה waitForAppToBeVisible("com.example.targetapp") מחכה שרכיב בממשק המשתמש עם שם החבילה שצוין יופיע במסך, במסגרת זמן קצוב לתפוגה שאפשר להתאים אישית.

// Wait for the app to be visible after launching it
startApp("com.example.targetapp")
waitForAppToBeVisible("com.example.targetapp")

כדי לוודא שממשק המשתמש של האפליקציה יציב לפני שמתקשרים איתו, משתמשים ב-waitForStable() API.

// Wait for the entire active window to become stable
activeWindow().waitForStable()

// Wait for a specific Ui element to become stable (e.g., after a loading animation)
onElement { viewIdResourceName == "my_loading_indicator" }.waitForStable()

שימוש ב-UI Automator לבדיקות Macrobenchmark ולפרופילים של Baseline

כדאי להשתמש ב-UI Automator לבדיקות ביצועים עם Jetpack Macrobenchmark וליצירת פרופילים Baseline, כי הוא מספק דרך אמינה ליצור אינטראקציה עם האפליקציה ולמדוד את הביצועים מנקודת המבט של משתמש הקצה.

‫Macrobenchmark משתמש ב-API של UI Automator כדי להפעיל את ממשק המשתמש ולמדוד אינטראקציות. לדוגמה, במדדי ביצועים של הפעלה, אפשר להשתמש ב-onElement כדי לזהות מתי התוכן של ממשק המשתמש נטען במלואו, וכך למדוד את הזמן עד להצגה מלאה (TTFD). במדדי השוואה של jank, נעשה שימוש בממשקי API של UI Automator כדי לגלול ברשימות או להפעיל אנימציות למדידת תזמוני פריימים. פונקציות כמו startActivity() או startIntent() שימושיות כדי להעביר את האפליקציה למצב הנכון לפני שהמדידה מתחילה.

כשיוצרים פרופילי Baseline, אתם מבצעים אוטומציה של תהליכי המשתמש הקריטיים (CUJ) באפליקציה כדי לתעד אילו מחלקות ושיטות דורשות קומפילציה מראש. ‫UI Automator הוא כלי אידיאלי לכתיבת סקריפטים כאלה לאוטומציה. החיפוש המודרני של רכיבים שמבוסס על פרדיקטים בשפת ה-DSL ומנגנוני ההמתנה המובנים (onElement) מובילים להרצת בדיקות חזקה ודטרמיניסטית יותר בהשוואה לשיטות אחרות. היציבות הזו מפחיתה את התנודתיות ומבטיחה שפרופיל ה-Baseline שנוצר משקף בצורה מדויקת את נתיבי הקוד שמופעלים במהלך תהליכי המשתמש החשובים ביותר.

תכונות מתקדמות

התכונות הבאות שימושיות לתרחישי בדיקה מורכבים יותר.

אינטראקציה עם כמה חלונות

ממשקי ה-API של UI Automator מאפשרים לקיים אינטראקציה ישירה עם רכיבי ממשק המשתמש ולבדוק אותם. התכונה הזו שימושית במיוחד בתרחישים שכוללים כמה חלונות, כמו מצב 'תמונה בתוך תמונה' (PiP) או פריסות של מסך מפוצל.

// Find the first window that is in Picture-in-Picture mode
val pipWindow = windows()
  .first { it.isInPictureInPictureMode == true }

// Now you can interact with elements within that specific window
pipWindow.onElement { textAsString() == "Play" }.click()

צילומי מסך וטענות נכוֹנוּת ויזואליות

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

uiautomator {
  // Take a screenshot of the entire active window
  val fullScreenBitmap: Bitmap = activeWindow().takeScreenshot()
  fullScreenBitmap.saveToFile(File("/sdcard/Download/full_screen.png"))

  // Take a screenshot of a specific UI element (e.g., a button)
  val buttonBitmap: Bitmap = onElement { viewIdResourceName == "my_button" }.takeScreenshot()
  buttonBitmap.saveToFile(File("/sdcard/Download/my_button_screenshot.png"))

  // Example: Take a screenshot of a PiP window
  val pipWindowScreenshot = windows()
    .first { it.isInPictureInPictureMode == true }
    .takeScreenshot()
  pipWindowScreenshot.saveToFile(File("/sdcard/Download/pip_screenshot.png"))
}

פונקציית התוסף saveToFile ל-Bitmap מפשטת את השמירה של התמונה שצולמה בנתיב שצוין.

שימוש ב-ResultsReporter לניפוי באגים

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

uiAutomator {
  startApp("com.example.targetapp")

  val reporter = ResultsReporter("MyTestArtifacts") // Name for this set of results
  val file = reporter.addNewFile(
    filename = "my_screenshot",
    title = "Accessible button image" // Title that appears in Android Studio test results
  )

  // Take a screenshot of an element and save it using the reporter
  onElement { textAsString() == "Accessible button" }
    .takeScreenshot()
    .saveToFile(file)

  // Report the artifacts to instrumentation, making them visible in Android Studio
  reporter.reportToInstrumentation()
}

מעבר מגרסאות ישנות יותר של UI Automator

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

סוג הפעולה שיטה ישנה של UI Automator שיטה חדשה ב-UI Automator
נקודת כניסה UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) עוטפים את לוגיקת הבדיקה בהיקף uiAutomator { ... }.
חיפוש רכיבים בממשק המשתמש device.findObject(By.res("com.example.app:id/my_button")) onElement { viewIdResourceName == "my\_button" }
חיפוש רכיבים בממשק המשתמש device.findObject(By.text("Click Me")) onElement { textAsString() == "Click Me" }
המתנה לממשק משתמש בלי פעילות device.waitForIdle() עדיפות למנגנון המובנה של פסק זמן ב-onElement, אחרת, activeWindow().waitForStable()
חיפוש אלמנטים משניים הוספה ידנית של תוספי שיחה findObject onElement().onElement() שרשור
טיפול בתיבות דו-שיח של הרשאות UiAutomator.registerWatcher() watchFor(PermissionDialog)