כתיבת בדיקות אוטומטיות באמצעות 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 { id == "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 { id == "first" }
      .onElement { id == "second" }
      .onElement { id == "third" }
      .click()
    
  • כדי לציין זמן קצוב לתפוגה לפונקציות onElement*, מעבירים ערך שמייצג אלפיות שנייה.

    // Find a Ui element with a zero timeout (instant check)
    onElement(0) { id == "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()

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

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

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

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

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 { id == "my_loading_indicator" }.waitForStable()

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

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

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

ממשקי ה-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 { id == "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 { id == "my\_button" }
חיפוש רכיבים בממשק המשתמש device.findObject(By.text("Click Me")) onElement { textAsString() == "Click Me" }
המתנה לממשק משתמש לא פעיל device.waitForIdle() עדיפות למנגנון המובנה של פסק זמן ב-onElement, אחרת activeWindow().waitForStable()
חיפוש רכיבי צאצא הוספה ידנית של findObject שיחות כרכיב צאצא onElement().onElement() שרשור
טיפול בתיבות דו-שיח של הרשאות UiAutomator.registerWatcher() watchFor(PermissionDialog)