Scrivere test automatici con Automator di UI

Il framework di test UI Automator fornisce un insieme di API per creare test dell'interfaccia utente che interagiscono con le app utente e di sistema.

Introduzione ai test moderni di UI Automator

UI Automator 2.4 introduce un linguaggio specifico del dominio (DSL) semplificato e compatibile con Kotlin che semplifica la scrittura di test UI per Android. Questa nuova superficie API si concentra sulla ricerca di elementi basata su predicati e sul controllo esplicito degli stati dell'app. Utilizzalo per creare test automatizzati più gestibili e affidabili.

UI Automator ti consente di testare un'app dall'esterno del processo dell'app. In questo modo puoi testare le versioni di release con la minimizzazione applicata. UI Automator è utile anche per scrivere test macrobenchmark.

Le caratteristiche principali dell'approccio moderno includono:

  • Un ambito di test uiAutomator dedicato per un codice di test più pulito ed espressivo.
  • Metodi come onElement, onElements e onElementOrNull per trovare elementi della UI con predicati chiari.
  • Meccanismo di attesa integrato per gli elementi condizionali onElement*(timeoutMs: Long = 10000)
  • Gestione esplicita dello stato dell'app, ad esempio waitForStable e waitForAppToBeVisible.
  • Interazione diretta con i nodi della finestra di accessibilità per scenari di test multi-finestra.
  • Funzionalità di screenshot integrate e un ResultsReporter per test visivi e debug.

Configura il progetto

Per iniziare a utilizzare le moderne API UI Automator, aggiorna il file build.gradle.kts del tuo progetto in modo da includere la dipendenza più recente:

Kotlin

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

Groovy

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

Concetti principali delle API

Le sezioni seguenti descrivono i concetti di base della moderna API UI Automator.

Ambito del test uiAutomator

Accedi a tutte le nuove API UI Automator all'interno del blocco uiAutomator { ... }. Questa funzione crea un UiAutomatorTestScope che fornisce un ambiente conciso e type-safe per le operazioni di test.

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

Trovare elementi UI

Utilizza le API UI Automator con i predicati per individuare gli elementi dell'interfaccia utente. Questi predicati ti consentono di definire le condizioni per proprietà come testo, stato selezionato o attivo e descrizione dei contenuti.

  • onElement { predicate }: restituisce il primo elemento UI che corrisponde al predicato entro un timeout predefinito. La funzione genera un'eccezione se non trova un elemento corrispondente.

    // 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 }: Simile a onElement, ma restituisce null se la funzione non trova alcun elemento corrispondente entro il timeout. Non genera un'eccezione. Utilizza questo metodo per gli elementi facoltativi.

    val optionalButton = onElementOrNull { textAsString() == "Skip" }
    optionalButton?.click() // Click only if the button exists
    
  • onElements { predicate }: attende che almeno un elemento UI corrisponda al predicato specificato, quindi restituisce un elenco di tutti gli elementi UI corrispondenti.

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

Ecco alcuni suggerimenti per l'utilizzo delle chiamate onElement:

  • Concatenare chiamate onElement per elementi nidificati: puoi concatenare chiamate onElement per trovare elementi all'interno di altri elementi, seguendo una gerarchia padre-figlio.

    // 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()
    
  • Specifica un timeout per le funzioni onElement* passando un valore che rappresenta i millisecondi.

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

Interagire con gli elementi dell'interfaccia utente

Interagisci con gli elementi UI simulando i clic o impostando il testo nei campi modificabili.

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

Gestire gli stati e i watcher delle app

Gestisci il ciclo di vita della tua app e gestisci gli elementi UI imprevisti che potrebbero apparire durante i test.

Gestione del ciclo di vita dell'app

Le API forniscono modi per controllare lo stato dell'app in fase di test:

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

Gestire l'interfaccia utente imprevista

L'API watchFor ti consente di definire i gestori per elementi UI imprevisti, come le finestre di dialogo delle autorizzazioni, che potrebbero apparire durante il flusso di test. Questo utilizza il meccanismo di monitoraggio interno, ma offre maggiore flessibilità.

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 è un esempio di ScopedWatcher<T>, dove T è l'oggetto passato come ambito al blocco in watchFor. Puoi creare watcher personalizzati in base a questo pattern.

Attendi la visibilità e la stabilità dell'app

A volte i test devono attendere che gli elementi diventino visibili o stabili. UI Automator offre diverse API per aiutarti in questo.

waitForAppToBeVisible("com.example.targetapp") attende che sullo schermo venga visualizzato un elemento UI con il nome del pacchetto specificato entro un timeout personalizzabile.

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

Utilizza l'API waitForStable() per verificare che l'interfaccia utente dell'app sia considerata stabile prima di interagire con essa.

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

Funzionalità avanzate

Le seguenti funzionalità sono utili per scenari di test più complessi.

Interagire con più finestre

Le API UI Automator ti consentono di interagire direttamente con gli elementi della UI e di esaminarli. Ciò è particolarmente utile per gli scenari che coinvolgono più finestre, come la modalità Picture in picture (PIP) o i layout a schermo diviso.

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

Screenshot e asserzioni visive

Acquisisci screenshot dell'intero schermo, di finestre specifiche o di singoli elementi dell'interfaccia utente direttamente nei test. Questo è utile per il test e il debug della regressione visiva.

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

La funzione di estensione saveToFile per Bitmap semplifica il salvataggio dell'immagine acquisita in un percorso specificato.

Utilizzare ResultsReporter per il debug

ResultsReporter ti aiuta ad associare gli artefatti di test, come gli screenshot, direttamente ai risultati dei test in Android Studio per semplificare l'ispezione e il debug.

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

Eseguire la migrazione da versioni precedenti di UI Automator

Se hai test UI Automator esistenti scritti con interfacce API precedenti, utilizza la seguente tabella come riferimento per la migrazione all'approccio moderno:

Tipo di azione Vecchio metodo UI Automator Nuovo metodo UI Automator
Punto di ingresso UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) Inserisci la logica di test nell'ambito uiAutomator { ... }.
Trovare elementi UI device.findObject(By.res("com.example.app:id/my_button")) onElement { id == "my\_button" }
Trovare elementi UI device.findObject(By.text("Click Me")) onElement { textAsString() == "Click Me" }
Attendi UI inattiva device.waitForIdle() Preferisci il meccanismo di timeout integrato di onElement; altrimenti, activeWindow().waitForStable()
Trovare gli elementi secondari Chiamate findObjectnidificate manualmente onElement().onElement() concatenamento
Gestire le finestre di dialogo delle autorizzazioni UiAutomator.registerWatcher() watchFor(PermissionDialog)