Rédiger des tests automatisés avec UI Automator

Le framework de test UI Automator fournit un ensemble d'API permettant de créer des tests d'interface utilisateur qui interagissent avec les applications utilisateur et système.

Présentation des tests UI Automator modernes

UI Automator 2.4 introduit un langage spécifique à un domaine (DSL) simplifié et compatible avec Kotlin, qui simplifie l'écriture de tests d'interface utilisateur pour Android. Cette nouvelle surface d'API se concentre sur la recherche d'éléments basée sur des prédicats et sur le contrôle explicite des états de l'application. Utilisez-la pour créer des tests automatisés plus fiables et plus faciles à gérer.

UI Automator vous permet de tester une application en dehors de son processus. Vous pouvez ainsi tester les versions à publier avec la minification appliquée. UI Automator est également utile pour écrire des tests macrobenchmark.

Voici les principales fonctionnalités de l'approche moderne :

  • Une portée de test uiAutomator dédiée pour un code de test plus propre et plus expressif.
  • Des méthodes telles que onElement, onElements et onElementOrNull pour trouver des éléments d'interface utilisateur avec des prédicats clairs.
  • Mécanisme d'attente intégré pour les éléments conditionnels onElement*(timeoutMs: Long = 10000)
  • Gestion explicite de l'état de l'application, par exemple waitForStable et waitForAppToBeVisible.
  • Interaction directe avec les nœuds de la fenêtre d'accessibilité pour les scénarios de test multifenêtre.
  • Fonctionnalités de capture d'écran intégrées et un ResultsReporter pour les tests visuels et le débogage.

Configurer votre projet

Pour commencer à utiliser les API UI Automator modernes, mettez à jour le fichier build.gradle.kts de votre projet afin d'inclure la dernière dépendance :

Kotlin

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

Groovy

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

Concepts clés de l'API

Les sections suivantes décrivent les concepts clés de l'API UI Automator moderne.

Portée du test uiAutomator

Accédez à toutes les nouvelles API UI Automator dans le uiAutomator { ... } bloc. Cette fonction crée un UiAutomatorTestScope qui fournit un environnement concis et de type sécurisé pour vos opérations de test.

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

Rechercher des éléments d'interface utilisateur

Utilisez les API UI Automator avec des prédicats pour localiser les éléments d'interface utilisateur. Ces prédicats vous permettent de définir des conditions pour des propriétés telles que le texte, l'état sélectionné ou sélectionné, et la description du contenu.

  • onElement { predicate } : renvoie le premier élément d'interface utilisateur qui correspond au prédicat dans un délai avant expiration par défaut. La fonction génère une exception si elle ne trouve pas d'élément correspondant.

    // 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 } : semblable à onElement, mais renvoie null si la fonction ne trouve aucun élément correspondant dans le délai avant expiration. Cela ne génère pas d'exception. Utilisez cette méthode pour les éléments facultatifs.

    val optionalButton = onElementOrNull { textAsString() == "Skip" }
    optionalButton?.click() // Click only if the button exists
    
  • onElements { predicate } : attend qu'au moins un élément d'interface utilisateur corresponde à le prédicat donné, puis renvoie une liste de tous les éléments d'interface utilisateur correspondants.

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

Voici quelques conseils pour utiliser les appels onElement :

  • Chaîner les appels onElement pour les éléments imbriqués : vous pouvez chaîner les appels onElement pour trouver des éléments dans d'autres éléments, en suivant une hiérarchie parent-enfant.

    // 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()
    
  • Spécifiez un délai avant expiration pour les fonctions onElement* en transmettant une valeur représentant millisecondes.

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

Interagir avec les éléments d'interface utilisateur

Interagissez avec les éléments d'interface utilisateur en simulant des clics ou en définissant du texte dans des champs modifiables.

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

Gérer les états et les observateurs de l'application

Gérez le cycle de vie de votre application et gérez les éléments d'interface utilisateur inattendus qui peuvent apparaître lors de vos tests.

Gestion du cycle de vie des applications

Les API permettent de contrôler l'état de l'application testée :

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

Gérer les interfaces utilisateur inattendues

L'API watchFor vous permet de définir des gestionnaires pour les éléments d'interface utilisateur inattendus, tels que les boîtes de dialogue d'autorisation, qui peuvent apparaître lors de votre flux de test. Elle utilise le mécanisme d'observateur interne, mais offre plus de flexibilité.

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 est un exemple de ScopedWatcher<T>, où T est l' objet transmis en tant que portée au bloc dans watchFor. Vous pouvez créer des observateurs personnalisés basés sur ce modèle.

Attendre la visibilité et la stabilité de l'application

Parfois, les tests doivent attendre que les éléments deviennent visibles ou stables. UI Automator propose plusieurs API pour vous aider.

Le waitForAppToBeVisible("com.example.targetapp") attend qu'un élément d'interface utilisateur portant le nom de package donné s'affiche à l'écran dans un délai avant expiration personnalisable.

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

Utilisez l'API waitForStable() pour vérifier que l'interface utilisateur de l'application est considérée comme stable avant d'interagir avec elle.

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

Utiliser UI Automator pour les macrobenchmarks et les profils de référence

Utilisez UI Automator pour les tests de performances avec Jetpack Macrobenchmark et pour générer des profils de référence, car il fournit un moyen fiable d' interagir avec votre application et de mesurer les performances du point de vue de l'utilisateur final.

Macrobenchmark utilise les API UI Automator pour piloter l'interface utilisateur et mesurer les interactions. Par exemple, dans les benchmarks de démarrage, vous pouvez utiliser onElement pour détecter quand le contenu de l'interface utilisateur est entièrement chargé, ce qui vous permet de mesurer le délai d'affichage total (TTFD). Dans les benchmarks de à-coups, les API UI Automator sont utilisées pour faire défiler des listes ou exécuter des animations afin de mesurer les durées des frames. Les fonctions telles que startActivity() ou startIntent() sont utiles pour que l'application soit dans l'état approprié avant le début de la mesure.

Lorsque vous générez des profils de référence, vous automatisez les parcours utilisateur critiques de votre application pour enregistrer les classes et les méthodes qui nécessitent une précompilation. UI Automator est un outil idéal pour écrire ces scripts d'automatisation. La recherche d'éléments basée sur des prédicats et les mécanismes d'attente intégrés (onElement) du DSL moderne permettent une exécution de test plus robuste et déterministe que les autres méthodes. Cette stabilité réduit l'instabilité et garantit que le profil de référence généré reflète avec précision les chemins de code exécutés lors de vos flux utilisateur les plus importants.

Fonctionnalités avancées

Les fonctionnalités suivantes sont utiles pour les scénarios de test plus complexes.

Interagir avec plusieurs fenêtres

Les API UI Automator vous permettent d'interagir directement avec les éléments d'interface utilisateur et de les inspecter. Cela est particulièrement utile dans les scénarios impliquant plusieurs fenêtres, comme le mode Picture-in-picture (PiP) ou les mises en page en écran partagé.

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

Captures d'écran et assertions visuelles

Capturez des captures d'écran de l'intégralité de l'écran, de fenêtres spécifiques ou d'éléments d'interface utilisateur individuels directement dans vos tests. Cela est utile pour les tests de régression visuelle et le débogage.

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

La fonction d'extension saveToFile pour Bitmap simplifie l'enregistrement de l'image capturée dans un chemin spécifié.

Utiliser ResultsReporter pour le débogage

Le ResultsReporter vous permet d'associer des artefacts de test, tels que des captures d'écran, directement à vos résultats de test dans Android Studio pour faciliter l'inspection et le débogage.

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

Migrer depuis des versions antérieures d'UI Automator

Si vous disposez de tests UI Automator existants écrits avec des surfaces d'API plus anciennes, utilisez le tableau suivant comme référence pour migrer vers l'approche moderne :

Type d'action Ancienne méthode UI Automator Nouvelle méthode UI Automator
Point d'entrée UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) Encapsulez la logique de test dans la portée uiAutomator { ... }.
Rechercher des éléments d'interface utilisateur device.findObject(By.res("com.example.app:id/my_button")) onElement { viewIdResourceName == "my\_button" }
Rechercher des éléments d'interface utilisateur device.findObject(By.text("Click Me")) onElement { textAsString() == "Click Me" }
Attendre l'inactivité de l'interface utilisateur device.waitForIdle() Privilégiez le mécanisme de délai avant expiration intégré à onElement ; sinon, activeWindow().waitForStable()
Rechercher des éléments enfants Appels findObject imbriqués manuellement onElement().onElement() chaînage
Gérer les boîtes de dialogue d'autorisation UiAutomator.registerWatcher() watchFor(PermissionDialog)