Criar testes automatizados com o UI Automator

O framework de testes do UI Automator oferece um conjunto de APIs para criar testes de IU que interagem com apps de usuário e do sistema.

Introdução aos testes modernos do UI Automator

O UI Automator 2.4 apresenta uma linguagem específica do domínio (DSL) simplificada e compatível com Kotlin que facilita a criação de testes de interface para Android. Essa nova plataforma de API se concentra na descoberta de elementos com base em predicados e no controle explícito dos estados do app. Use para criar testes automatizados mais confiáveis e fáceis de manter.

O UI Automator permite testar um app de fora do processo dele. Isso permite testar versões de lançamento com minificação aplicada. O UI Automator também ajuda na criação de testes de macrobenchmark.

Os principais recursos da abordagem moderna incluem:

  • Um escopo de teste uiAutomator dedicado para um código de teste mais limpo e expressivo.
  • Métodos como onElement, onElements e onElementOrNull para encontrar elementos de interface com predicados claros.
  • Mecanismo de espera integrado para elementos condicionais onElement*(timeoutMs: Long = 10000)
  • Gerenciamento explícito do estado do app, como waitForStable e waitForAppToBeVisible.
  • Interação direta com nós de janela de acessibilidade para cenários de teste com várias janelas.
  • Recursos integrados de captura de tela e um ResultsReporter para testes visuais e depuração.

Configurar seu projeto

Para começar a usar as APIs modernas do UI Automator, atualize o arquivo build.gradle.kts do projeto para incluir a dependência mais recente:

Kotlin

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

Groovy

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

Principais conceitos da API

As seções a seguir descrevem os conceitos principais da API UI Automator moderna.

O escopo de teste do uiAutomator

Acesse todas as novas APIs do UI Automator no bloco uiAutomator { ... }. Essa função cria um UiAutomatorTestScope que fornece um ambiente conciso e com segurança de tipo para suas operações de teste.

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

Encontrar elementos da interface

Use as APIs do UI Automator com predicados para localizar elementos da interface. Esses predicados permitem definir condições para propriedades como texto, estado selecionado ou em foco e descrição do conteúdo.

  • onElement { predicate }: retorna o primeiro elemento da interface que corresponde ao predicado em um tempo limite padrão. A função gera uma exceção se não encontrar um elemento correspondente.

    // 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 }: semelhante a onElement, mas retorna null se a função não encontrar nenhum elemento correspondente dentro do tempo limite. Isso não gera uma exceção. Use esse método para elementos opcionais.

    val optionalButton = onElementOrNull { textAsString() == "Skip" }
    optionalButton?.click() // Click only if the button exists
    
  • onElements { predicate }: aguarda até que pelo menos um elemento da interface corresponda ao predicado especificado e retorna uma lista de todos os elementos correspondentes.

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

Confira algumas dicas para usar chamadas onElement:

  • Encadeie chamadas onElement para elementos aninhados: você pode encadear chamadas onElement para encontrar elementos dentro de outros elementos, seguindo uma hierarquia pai-filho.

    // 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()
    
  • Especifique um tempo limite para funções onElement* transmitindo um valor que representa milissegundos.

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

Interagir com elementos da interface

Interaja com elementos da interface simulando cliques ou definindo texto em campos editáveis.

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

Processar estados e observadores de apps

Gerencie o ciclo de vida do app e processe elementos inesperados da interface que podem aparecer durante os testes.

Gerenciamento do ciclo de vida do app

As APIs oferecem maneiras de controlar o estado do app em teste:

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

Gerenciar interfaces inesperadas

A API watchFor permite definir manipuladores para elementos inesperados da interface, como caixas de diálogo de permissão, que podem aparecer durante o fluxo de teste. Isso usa o mecanismo de observador interno, mas oferece mais flexibilidade.

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 é um exemplo de ScopedWatcher<T>, em que T é o objeto transmitido como um escopo para o bloco em watchFor. É possível criar observadores personalizados com base nesse padrão.

Aguardar a visibilidade e a estabilidade do app

Às vezes, os testes precisam esperar que os elementos fiquem visíveis ou estáveis. O UI Automator oferece várias APIs para ajudar nisso.

O waitForAppToBeVisible("com.example.targetapp") aguarda que um elemento da interface com o nome do pacote especificado apareça na tela dentro de um tempo limite personalizável.

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

Use a API waitForStable() para verificar se a interface do app é considerada estável antes de interagir com ela.

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

Recursos avançados

Os recursos a seguir são úteis para cenários de teste mais complexos.

Interagir com várias janelas

As APIs do UI Automator permitem interagir diretamente com elementos da interface e inspecioná-los. Isso é especialmente útil para cenários que envolvem várias janelas, como o modo picture-in-picture (PiP) ou layouts de tela dividida.

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

Capturas de tela e declarações visuais

Capture capturas de tela da tela inteira, de janelas específicas ou de elementos individuais da interface diretamente nos testes. Isso é útil para testes e depuração de regressão visual.

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

A função de extensão saveToFile para Bitmap simplifica o salvamento da imagem capturada em um caminho especificado.

Usar o ResultsReporter para depuração

O ResultsReporter ajuda você a associar artefatos de teste, como capturas de tela, diretamente aos resultados dos testes no Android Studio para facilitar a inspeção e a depuração.

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

Migrar de versões mais antigas do UI Automator

Se você tiver testes do UI Automator escritos com interfaces de API mais antigas, use a tabela a seguir como referência para migrar para a abordagem moderna:

Tipo de ação Método antigo do UI Automator Novo método do UI Automator
Ponto de entrada UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) Envolva a lógica de teste no escopo uiAutomator { ... }.
Encontrar elementos da interface device.findObject(By.res("com.example.app:id/my_button")) onElement { id == "my\_button" }
Encontrar elementos da interface device.findObject(By.text("Click Me")) onElement { textAsString() == "Click Me" }
Aguardar a UI inativa device.waitForIdle() Prefira o mecanismo de tempo limite integrado do onElement. Caso contrário, activeWindow().waitForStable().
Encontrar elementos filhos Chamadas findObject aninhadas manualmente onElement().onElement() encadeamento
Processar caixas de diálogo de permissões UiAutomator.registerWatcher() watchFor(PermissionDialog)