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

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

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

בגרסה UI Automator 2.4, הוספנו שפה ספציפית לדומיין (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)