Automatisierte Tests mit UI Automator schreiben

Das UI Automator-Testframework bietet eine Reihe von APIs zum Erstellen von UI-Tests, die mit Nutzer- und System-Apps interagieren.

Einführung in moderne UI Automator-Tests

UI Automator 2.4 führt eine optimierte, Kotlin-freundliche domänenspezifische Sprache (Domain Specific Language, DSL) ein, die das Schreiben von UI-Tests für Android vereinfacht. Diese neue API Oberfläche konzentriert sich auf die prädikatbasierte Elementfindung und die explizite Steuerung von App Zuständen. Damit können Sie wartungsfreundlichere und zuverlässigere automatisierte Tests erstellen.

Mit UI Automator können Sie eine App außerhalb des App-Prozesses testen. So können Sie Release-Versionen mit angewendeter Reduzierung testen. UI Automator ist auch beim Schreiben von Makrobenchmark-Tests hilfreich.

Zu den wichtigsten Funktionen des modernen Ansatzes gehören:

  • Ein dedizierter uiAutomator Testbereich für saubereren und aussagekräftigeren Test code.
  • Methoden wie onElement, onElements und onElementOrNull zum Suchen von UI-Elementen mit eindeutigen Prädikaten.
  • Integrierter Wartemechanismus für bedingte Elemente onElement*(timeoutMs: Long = 10000).
  • Explizite Verwaltung des App-Zustands wie waitForStable und waitForAppToBeVisible.
  • Direkte Interaktion mit Knoten von Barrierefreiheitsfenstern für Mehrfenster-Testszenarien.
  • Integrierte Screenshot-Funktionen und ein ResultsReporter für visuelle Tests und Fehlerbehebung.

Projekt einrichten

Wenn Sie die modernen UI Automator-APIs verwenden möchten, aktualisieren Sie die Datei Ihres Projekts build.gradle.kts und fügen Sie die neueste Abhängigkeit hinzu:

Kotlin

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

Groovy

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

Grundlegende API-Konzepte

In den folgenden Abschnitten werden die grundlegenden Konzepte der modernen UI Automator API beschrieben.

Der uiAutomator-Testbereich

Greifen Sie auf alle neuen UI Automator-APIs innerhalb des uiAutomator { ... }-Blocks zu. Mit dieser Funktion wird ein UiAutomatorTestScope erstellt, der eine prägnante und typsichere Umgebung für Ihre Testvorgänge bietet.

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

UI-Elemente suchen

Verwenden Sie UI Automator-APIs mit Prädikaten, um UI-Elemente zu finden. Mit diesen Prädikaten können Sie Bedingungen für Eigenschaften wie Text, ausgewählter oder fokussierter Zustand und Inhaltsbeschreibung definieren.

  • onElement { predicate }: Gibt das erste UI-Element zurück, das innerhalb eines Standardzeitlimits mit dem Prädikat übereinstimmt. Die Funktion löst eine Ausnahme aus, wenn es kein übereinstimmendes Element findet.

    // 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 }: Ähnlich wie onElement, gibt aber null zurück, wenn die Funktion innerhalb des Zeitlimits kein übereinstimmendes Element findet. Es wird keine Ausnahme ausgelöst. Verwenden Sie diese Methode für optionale Elemente.

    val optionalButton = onElementOrNull { textAsString() == "Skip" }
    optionalButton?.click() // Click only if the button exists
    
  • onElements { predicate }: Wartet, bis mindestens ein UI-Element mit dem angegebenen Prädikat übereinstimmt, und gibt dann eine Liste aller übereinstimmenden UI-Elemente zurück.

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

Hier einige Tipps zur Verwendung von onElement-Aufrufen:

  • onElement-Aufrufe für verschachtelte Elemente verketten: Sie können onElement Aufrufe verketten, um Elemente in anderen Elementen zu finden, wobei eine Über-/Untergeordnet- Hierarchie verwendet wird.

    // 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()
    
  • Geben Sie ein Zeitlimit für onElement*-Funktionen an, indem Sie einen Wert übergeben, der Millisekunden darstellt.

    // 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()
    

Mit UI-Elementen interagieren

Sie können mit UI-Elementen interagieren, indem Sie Klicks simulieren oder Text in bearbeitbare Felder eingeben.

// 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()

App-Zustände und Watcher verarbeiten

Verwalten Sie den Lebenszyklus Ihrer App und verarbeiten Sie unerwartete UI-Elemente, die während Ihrer Tests angezeigt werden können.

Verwaltung des App-Lebenszyklus

Die APIs bieten Möglichkeiten, den Zustand der zu testenden App zu steuern:

// 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")

Unerwartete UI verarbeiten

Mit der watchFor API können Sie Handler für unerwartete UI-Elemente wie Berechtigungsdialogfelder definieren, die während des Testablaufs angezeigt werden können. Dabei wird der interne Watcher-Mechanismus verwendet, doch es wird mehr Flexibilität geboten.

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 ist ein Beispiel für einen ScopedWatcher<T>, wobei T das Objekt ist, das als Bereich an den Block in watchFor übergeben wird. Sie können benutzerdefinierte Watcher basierend auf diesem Muster erstellen.

Auf App-Sichtbarkeit und -Stabilität warten

Manchmal müssen Tests warten, bis Elemente sichtbar oder stabil werden. UI Automator bietet mehrere APIs, die dabei helfen.

The waitForAppToBeVisible("com.example.targetapp") wartet innerhalb eines anpassbaren Zeitlimits, bis ein UI-Element mit dem angegebenen Paketnamen auf dem Bildschirm angezeigt wird.

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

Verwenden Sie die waitForStable() API, um zu prüfen, ob die UI der App als stabil gilt bevor Sie mit ihr interagieren.

// 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 für Makrobenchmarks und Baseline Profiles verwenden

Verwenden Sie UI Automator für Leistungstests mit Jetpack Macrobenchmark und zum Generieren von Baseline Profiles, da es eine zuverlässige Möglichkeit bietet, mit Ihrer App zu interagieren und die Leistung aus Endnutzersicht zu messen.

Macrobenchmark verwendet UI Automator-APIs, um die UI zu steuern und Interaktionen zu messen. Bei Startbenchmarks können Sie beispielsweise onElement verwenden, um zu erkennen, wann UI Inhalte vollständig geladen sind, sodass Sie die Zeit bis zur vollständigen Anzeige (Time to Full Display TTFD) messen können. Bei Jank-Benchmarks werden UI Automator-APIs verwendet, um Listen zu scrollen oder Animationen auszuführen, um die Frame-Zeiten zu messen. Funktionen wie startActivity() oder startIntent() sind nützlich, um die App in den richtigen Zustand zu versetzen, bevor die Messung beginnt.

Beim Generieren von Baseline Profiles automatisieren Sie die kritischen Nutzer prozesse (Critical User Journeys, CUJs) Ihrer App, um aufzuzeichnen, welche Klassen und Methoden vorkompiliert werden müssen. UI Automator ist ein ideales Tool zum Schreiben dieser Automatisierungsskripts. Die prädikatbasierte Elementfindung und die integrierten Wartemechanismen (onElement) der modernen DSL's führen im Vergleich zu anderen Methoden zu einer robusteren und deterministischeren Testausführung. Diese Stabilität reduziert die Instabilität und sorgt dafür, dass das generierte Baseline Profile die Codepfade genau widerspiegelt, die während Ihrer wichtigsten Nutzer prozesse ausgeführt werden.

Erweiterte Funktionen

Die folgenden Funktionen sind für komplexere Testszenarien nützlich.

Mit mehreren Fenstern interagieren

Mit den UI Automator-APIs können Sie direkt mit UI Elementen interagieren und sie prüfen. Dies ist besonders nützlich für Szenarien mit mehreren Fenstern, z. B. im Bild-in-Bild-Modus (Picture-in-Picture, PiP) oder bei Splitscreen-Layouts.

// 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()

Screenshots und visuelle Assertions

Sie können Screenshots des gesamten Bildschirms, bestimmter Fenster oder einzelner UI-Elemente direkt in Ihren Tests erstellen. Das ist hilfreich für visuelle Regressionstests und das Debugging.

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"))
}

Mit der saveToFile Erweiterungsfunktion für Bitmap lässt sich das aufgenommene Bild einfach unter einem angegebenen Pfad speichern.

ResultsReporter für das Debugging verwenden

Mit ResultsReporter können Sie Testartefakte wie Screenshots direkt mit Ihren Testergebnissen in Android Studio verknüpfen, um die Überprüfung und das Debugging zu erleichtern.

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()
}

Von älteren UI Automator-Versionen migrieren

Wenn Sie vorhandene UI Automator-Tests mit älteren API-Oberflächen geschrieben haben, können Sie die folgende Tabelle als Referenz für die Migration zum modernen Ansatz verwenden:

Aktionstyp Alte UI Automator-Methode Neue UI Automator-Methode
Einstiegspunkt UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) Testlogik in den uiAutomator { ... } Bereich einschließen.
UI-Elemente suchen device.findObject(By.res("com.example.app:id/my_button")) onElement { viewIdResourceName == "my\_button" }
UI-Elemente suchen device.findObject(By.text("Click Me")) onElement { textAsString() == "Click Me" }
Auf inaktive UI warten device.waitForIdle() Bevorzugen des integrierten Zeitlimitmechanismus von onElement; andernfalls activeWindow().waitForStable()
Untergeordnete Elemente suchen Manuell verschachtelte findObject-Aufrufe onElement().onElement()-Verkettung
Berechtigungsdialogfelder verarbeiten UiAutomator.registerWatcher() watchFor(PermissionDialog)