Criar testes automatizados com o UI Automator

O UI Automator é um framework de testes de interface adequado para testes funcionais da interface entre apps instalados e do sistema. As APIs UI Automator permitem que você interaja com elementos visíveis em um dispositivo, independente de qual Activity está em foco. Isso permite realizar operações como abrir o menu Configurações ou a tela de início em um dispositivo de teste. Seu teste pode procurar um componente de interface usando descritores convenientes, como o texto mostrado nesse componente ou a descrição do conteúdo dele.

O framework de testes UI Automator é uma API baseada em instrumentação e funciona com o executor de testes AndroidJUnitRunner. Ele é adequado para programar testes automatizados no estilo caixa opaco, em que o código do teste não depende de detalhes de implementação internos do app de destino.

Os principais recursos do framework de testes do UI Automator incluem:

Como acessar o estado do dispositivo

O framework de testes UI Automator fornece uma classe UiDevice para acessar e realizar operações no dispositivo em que o app de destino está sendo executado. Você pode chamar os métodos dela para acessar propriedades do dispositivo, como orientação atual ou tamanho da tela. A classe UiDevice também permite executar as seguintes ações:

  1. Mudar a rotação do dispositivo.
  2. Pressionar as teclas de hardware, como "aumentar volume".
  3. Pressionar os botões "Voltar", "Início" ou "Menu".
  4. Abrir a aba de notificações.
  5. Fazer uma captura de tela da janela atual.

Por exemplo, para simular o pressionamento do botão home, chame o método UiDevice.pressHome().

APIs do UI Automator

As APIs UI Automator permitem criar testes robustos sem precisar conhecer os detalhes de implementação do app de destino. Você pode usar essas APIs para capturar e manipular componentes da interface em vários apps:

  • UiObject2: representa um elemento da interface visível no dispositivo.
  • BySelector: especifica critérios para elementos de IU correspondentes.
  • By: cria BySelector de forma concisa.
  • Configurator: permite definir parâmetros importantes para executar testes do UI Automator.

Por exemplo, o código a seguir mostra como escrever um script de teste que abre um app Gmail no dispositivo:

Kotlin


device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
device.pressHome()

val gmail: UiObject2 = device.findObject(By.text("Gmail"))
// Perform a click and wait until the app is opened.
val opened: Boolean = gmail.clickAndWait(Until.newWindow(), 3000)
assertThat(opened).isTrue()

Java


device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
device.pressHome();

UiObject2 gmail = device.findObject(By.text("Gmail"));
// Perform a click and wait until the app is opened.
Boolean opened = gmail.clickAndWait(Until.newWindow(), 3000);
assertTrue(opened);

Configurar o UI Automator

Antes de criar seu teste de IU com o UI Automator, configure o local do código-fonte do teste e as dependências do projeto, conforme descrito em Configurar projetos para o AndroidX Test.

No arquivo build.gradle do módulo do app Android, é necessário definir uma referência de dependência para a biblioteca do UI Automator:

Kotlin

dependencies {
  ...
  androidTestImplementation('androidx.test.uiautomator:uiautomator:2.3.0-alpha03')
}

Groovy

dependencies {
  ...
  androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.3.0-alpha03'
}

Para otimizar os testes do UI Automator, inspecione primeiro os componentes da interface do app de destino e verifique se eles podem ser acessados. Essas dicas de otimização são descritas nas próximas duas seções.

Inspecionar a IU em um dispositivo

Antes de projetar seu teste, inspecione os componentes da interface visíveis no dispositivo. Para garantir que os testes do UI Automator possam acessar esses componentes, verifique se eles têm rótulos de texto, valores android:contentDescription ou ambos.

A ferramenta uiautomatorviewer oferece uma interface visual conveniente para inspecionar a hierarquia de layouts e conferir as propriedades dos componentes de interface visíveis no primeiro plano do dispositivo. Essas informações permitem criar testes mais refinados usando o UI Automator. Por exemplo, você pode criar um seletor de IU que corresponda a uma propriedade visível específica.

Para iniciar a ferramenta uiautomatorviewer:

  1. Abra o app de destino em um dispositivo físico.
  2. Conecte o dispositivo à máquina de desenvolvimento.
  3. Abra uma janela de terminal e navegue até o diretório <android-sdk>/tools/.
  4. Execute a ferramenta com este comando:
 $ uiautomatorviewer

Para ver as propriedades de IU do app:

  1. Na interface do uiautomatorviewer, clique no botão Device screenshot.
  2. Passe o cursor sobre o snapshot no painel esquerdo para ver os componentes de IU identificados pela ferramenta uiautomatorviewer. As propriedades são listadas no painel inferior direito e a hierarquia de layout no painel superior direito.
  3. Se quiser, clique no botão Toggle NAF Nodes para ver os componentes de IU que não podem ser acessados pelo UI Automator. Apenas informações limitadas podem estar disponíveis para esses componentes.

Para saber mais sobre os tipos comuns de componentes de IU oferecidos pelo Android, consulte Interface do usuário.

Garantir que a atividade esteja acessível

O framework de testes UI Automator tem uma performance melhor em apps que implementaram recursos de acessibilidade do Android. Quando você usa elementos da interface do tipo View ou uma subclasse de View do SDK, não é necessário implementar suporte à acessibilidade, porque essas classes já fazem isso.

No entanto, alguns apps usam elementos de interface personalizados para oferecer uma experiência mais rica ao usuário. Esses elementos não oferecem compatibilidade automática com acessibilidade. Se o app tiver instâncias de uma subclasse da View que não é do SDK, adicione recursos de acessibilidade a esses elementos seguindo estas etapas:

  1. Crie uma classe concreta que estenda ExploreByTouchHelper.
  2. Chame setAccessibilityDelegate() para associar uma instância da nova classe a um elemento de IU personalizado específico.

Para mais orientações sobre como adicionar recursos de acessibilidade a elementos de visualização personalizados, consulte Como criar visualizações personalizadas acessíveis. Para saber mais sobre práticas recomendadas gerais para acessibilidade no Android, consulte Como tornar apps mais acessíveis.

Criar uma classe de teste do UI Automator

A classe de teste do UI Automator precisa ser programada da mesma forma que uma classe de teste do JUnit 4. Para saber mais sobre a criação de classes de teste do JUnit 4 e o uso de declarações e anotações do JUnit 4, consulte Criar uma classe de teste de unidade de instrumentação.

Adicione a anotação @RunWith(AndroidJUnit4.class) no início da definição da classe de teste. Também é necessário especificar a classe AndroidJUnitRunner, fornecida no AndroidX Test, como o executor de testes padrão. Essa etapa é descrita em mais detalhes em Executar testes do UI Automator em um dispositivo ou emulador.

Implemente o seguinte modelo de programação na classe de teste do UI Automator:

  1. Receba um objeto UiDevice para acessar o dispositivo que você quer testar chamando o método getInstance() e transmitindo um objeto Instrumentation como argumento.
  2. Acesse um objeto UiObject2 para acessar um componente de IU que é exibido no dispositivo (por exemplo, a visualização atual em primeiro plano), chamando o método findObject().
  3. Simule uma interação específica do usuário para realizar nesse componente de IU chamando um método UiObject2. Por exemplo, chame scrollUntil() para rolar e setText() para editar um campo de texto. Você pode chamar as APIs nas etapas 2 e 3 repetidamente conforme necessário para testar interações mais complexas do usuário que envolvem vários componentes de interface ou sequências de ações do usuário.
  4. Verifique se a interface reflete o estado ou o comportamento esperado depois que essas interações do usuário forem realizadas.

Essas etapas são abordadas com mais detalhes nas seções abaixo.

Acessar componentes de IU

O objeto UiDevice é a principal maneira de acessar e manipular o estado do dispositivo. Nos testes, você pode chamar métodos UiDevice para verificar o estado de várias propriedades, como a orientação atual ou o tamanho da tela. Seu teste pode usar o objeto UiDevice para realizar ações no dispositivo, como forçar o dispositivo a uma rotação específica, pressionar os botões D-pad do hardware e os botões home e Menu.

Uma prática recomendada é iniciar o teste na tela inicial do dispositivo. Na tela inicial (ou em algum outro local de partida escolhido no dispositivo), é possível chamar os métodos disponibilizados pela API UI Automator para selecionar e interagir com elementos específicos da IU.

O snippet de código a seguir mostra como o teste pode receber uma instância de UiDevice e simular o pressionamento do botão home:

Kotlin


import org.junit.Before
import androidx.test.runner.AndroidJUnit4
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.By
import androidx.test.uiautomator.Until
...

private const val BASIC_SAMPLE_PACKAGE = "com.example.android.testing.uiautomator.BasicSample"
private const val LAUNCH_TIMEOUT = 5000L
private const val STRING_TO_BE_TYPED = "UiAutomator"

@RunWith(AndroidJUnit4::class)
@SdkSuppress(minSdkVersion = 18)
class ChangeTextBehaviorTest2 {

private lateinit var device: UiDevice

@Before
fun startMainActivityFromHomeScreen() {
  // Initialize UiDevice instance
  device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())

  // Start from the home screen
  device.pressHome()

  // Wait for launcher
  val launcherPackage: String = device.launcherPackageName
  assertThat(launcherPackage, notNullValue())
  device.wait(
    Until.hasObject(By.pkg(launcherPackage).depth(0)),
    LAUNCH_TIMEOUT
  )

  // Launch the app
  val context = ApplicationProvider.getApplicationContext<Context>()
  val intent = context.packageManager.getLaunchIntentForPackage(
  BASIC_SAMPLE_PACKAGE).apply {
    // Clear out any previous instances
    addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
  }
  context.startActivity(intent)

  // Wait for the app to appear
  device.wait(
    Until.hasObject(By.pkg(BASIC_SAMPLE_PACKAGE).depth(0)),
    LAUNCH_TIMEOUT
    )
  }
}

Java


import org.junit.Before;
import androidx.test.runner.AndroidJUnit4;
import androidx.test.uiautomator.UiDevice;
import androidx.test.uiautomator.By;
import androidx.test.uiautomator.Until;
...

@RunWith(AndroidJUnit4.class)
@SdkSuppress(minSdkVersion = 18)
public class ChangeTextBehaviorTest {

  private static final String BASIC_SAMPLE_PACKAGE
  = "com.example.android.testing.uiautomator.BasicSample";
  private static final int LAUNCH_TIMEOUT = 5000;
  private static final String STRING_TO_BE_TYPED = "UiAutomator";
  private UiDevice device;

  @Before
  public void startMainActivityFromHomeScreen() {
    // Initialize UiDevice instance
    device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());

    // Start from the home screen
    device.pressHome();

    // Wait for launcher
    final String launcherPackage = device.getLauncherPackageName();
    assertThat(launcherPackage, notNullValue());
    device.wait(Until.hasObject(By.pkg(launcherPackage).depth(0)),
    LAUNCH_TIMEOUT);

    // Launch the app
    Context context = ApplicationProvider.getApplicationContext();
    final Intent intent = context.getPackageManager()
    .getLaunchIntentForPackage(BASIC_SAMPLE_PACKAGE);
    // Clear out any previous instances
    intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
    context.startActivity(intent);

    // Wait for the app to appear
    device.wait(Until.hasObject(By.pkg(BASIC_SAMPLE_PACKAGE).depth(0)),
    LAUNCH_TIMEOUT);
    }
}

No exemplo, a instrução @SdkSuppress(minSdkVersion = 18) ajuda a garantir que os testes sejam executados apenas em dispositivos com o Android 4.3 (API de nível 18) ou versões mais recentes, conforme exigido pelo framework UI Automator.

Use o método findObject() para extrair um UiObject2 que representa uma visualização correspondente a um determinado critério de seleção. É possível reutilizar as instâncias de UiObject2 que você criou em outras partes do teste do app, conforme necessário. Observe que o framework de teste do UI Automator pesquisa uma correspondência na exibição atual sempre que o teste usa uma instância UiObject2 para clicar em um elemento da IU ou consultar uma propriedade.

O snippet a seguir mostra como o teste pode criar instâncias UiObject2 que representam um botão "Cancelar" e um botão "OK" em um app.

Kotlin


val okButton: UiObject2 = device.findObject(
    By.text("OK").clazz("android.widget.Button")
)

// Simulate a user-click on the OK button, if found.
if (okButton != null) {
    okButton.click()
}

Java


UiObject2 okButton = device.findObject(
    By.text("OK").clazz("android.widget.Button")
);

// Simulate a user-click on the OK button, if found.
if (okButton != null) {
    okButton.click();
}

Especificar um seletor

Para acessar um componente de interface específico em um app, use a classe By para construir uma instância de BySelector. BySelector representa uma consulta para elementos específicos na IU exibida.

Se mais de um elemento correspondente for encontrado, o primeiro deles na hierarquia de layout vai ser retornado como o UiObject2 de destino. Ao criar um BySelector, é possível encadear várias propriedades para refinar a pesquisa. Se nenhum elemento de interface correspondente for encontrado, uma null será retornada.

Use o método hasChild() ou hasDescendant() para aninhar várias instâncias BySelector. O exemplo de código abaixo mostra como o teste pode especificar uma pesquisa para encontrar o primeiro ListView que tem um elemento de interface filho com a propriedade de texto.

Kotlin


val listView: UiObject2 = device.findObject(
    By.clazz("android.widget.ListView")
        .hasChild(
            By.text("Apps")
        )
)

Java


UiObject2 listView = device.findObject(
    By.clazz("android.widget.ListView")
        .hasChild(
            By.text("Apps")
        )
);

Pode ser útil especificar o estado do objeto nos seus critérios de seleção. Por exemplo, se você quiser selecionar uma lista de todos os elementos marcados para desmarcá-los, chame o método checked() com o argumento definido como verdadeiro.

Realizar ações

Quando o teste receber um objeto UiObject2, chame os métodos na classe UiObject2 para realizar interações do usuário no componente de interface representado por esse objeto. É possível especificar ações como:

  • click() : clica no centro dos limites visíveis do elemento da interface.
  • drag() : arrasta esse objeto para coordenadas arbitrárias.
  • setText() : define o texto em um campo editável depois de limpar o conteúdo do campo. Por outro lado, o método clear() limpa o texto existente em um campo editável.
  • swipe() : realiza a ação de deslizar na direção especificada.
  • scrollUntil(): executa a ação de rolagem em direção à direção especificada até que Condition ou EventCondition seja atendido.

O framework de testes UI Automator permite enviar uma intent ou iniciar uma Activity sem usar comandos do shell, recebendo um objeto Context usando getContext().

O snippet a seguir mostra como o teste pode usar uma intent para iniciar o app em teste. Essa abordagem é útil quando você só tem interesse em testar o app de calculadora e não se importa com a tela de início.

Kotlin


fun setUp() {
...

  // Launch a simple calculator app
  val context = getInstrumentation().context
  val intent = context.packageManager.getLaunchIntentForPackage(CALC_PACKAGE).apply {
    addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
  }
  // Clear out any previous instances
  context.startActivity(intent)
  device.wait(Until.hasObject(By.pkg(CALC_PACKAGE).depth(0)), TIMEOUT)
}

Java


public void setUp() {
...

  // Launch a simple calculator app
  Context context = getInstrumentation().getContext();
  Intent intent = context.getPackageManager()
  .getLaunchIntentForPackage(CALC_PACKAGE);
  intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);

  // Clear out any previous instances
  context.startActivity(intent);
  device.wait(Until.hasObject(By.pkg(CALC_PACKAGE).depth(0)), TIMEOUT);
}

Verificar resultados

O InstrumentationTestCase estende o TestCase. Assim, é possível usar métodos Assert JUnit padrão para testar se os componentes da IU no app retornam os resultados esperados.

O snippet a seguir mostra como o teste pode localizar vários botões em um app de calculadora, clicar neles em ordem e verificar se o resultado correto é exibido.

Kotlin


private const val CALC_PACKAGE = "com.myexample.calc"

fun testTwoPlusThreeEqualsFive() {
  // Enter an equation: 2 + 3 = ?
  device.findObject(By.res(CALC_PACKAGE, "two")).click()
  device.findObject(By.res(CALC_PACKAGE, "plus")).click()
  device.findObject(By.res(CALC_PACKAGE, "three")).click()
  device.findObject(By.res(CALC_PACKAGE, "equals")).click()

  // Verify the result = 5
  val result: UiObject2 = device.findObject(By.res(CALC_PACKAGE, "result"))
  assertEquals("5", result.text)
}

Java


private static final String CALC_PACKAGE = "com.myexample.calc";

public void testTwoPlusThreeEqualsFive() {
  // Enter an equation: 2 + 3 = ?
  device.findObject(By.res(CALC_PACKAGE, "two")).click();
  device.findObject(By.res(CALC_PACKAGE, "plus")).click();
  device.findObject(By.res(CALC_PACKAGE, "three")).click();
  device.findObject(By.res(CALC_PACKAGE, "equals")).click();

  // Verify the result = 5
  UiObject2 result = device.findObject(By.res(CALC_PACKAGE, "result"));
  assertEquals("5", result.getText());
}

Executar testes do UI Automator em um dispositivo ou emulador

Você pode executar testes do UI Automator no Android Studio ou na linha de comando. Especifique AndroidJUnitRunner como o executor de instrumentação padrão no projeto.

Mais exemplos

Interagir com a interface do sistema

O UI Automator pode interagir com tudo na tela, incluindo elementos do sistema fora do app, conforme mostrado nos snippets de código abaixo:

Kotlin


// Opens the System Settings.
device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
device.executeShellCommand("am start -a android.settings.SETTINGS")

Java


// Opens the System Settings.
device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
device.executeShellCommand("am start -a android.settings.SETTINGS");

Kotlin


// Opens the notification shade.
device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
device.openNotification()

Java


// Opens the notification shade.
device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
device.openNotification();

Kotlin


// Opens the Quick Settings shade.
device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
device.openQuickSettings()

Java


// Opens the Quick Settings shade.
device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
device.openQuickSettings();

Kotlin


// Get the system clock.
device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
UiObject2 clock = device.findObject(By.res("com.android.systemui:id/clock"))
print(clock.getText())

Java


// Get the system clock.
device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
UiObject2 clock = device.findObject(By.res("com.android.systemui:id/clock"));
print(clock.getText());

Aguardar transições

Desativar o modo &quot;Não perturbe&quot;
Figura 1. O UI Automator desativa o modo Não perturbe em um dispositivo de teste.

As transições de tela podem levar algum tempo e a previsão da duração delas não é confiável. Por isso, o UI Automator precisa esperar depois de executar operações. O UI Automator oferece vários métodos para isso:

O snippet de código abaixo mostra como usar o UI Automator para desativar o modo Não perturbe nas configurações do sistema usando o método performActionAndWait() que aguarda transições:

Kotlin


@Test
@SdkSuppress(minSdkVersion = 21)
@Throws(Exception::class)
fun turnOffDoNotDisturb() {
    device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
    device.performActionAndWait({
        try {
            device.executeShellCommand("am start -a android.settings.SETTINGS")
        } catch (e: IOException) {
            throw RuntimeException(e)
        }
    }, Until.newWindow(), 1000)
    // Check system settings has been opened.
    Assert.assertTrue(device.hasObject(By.pkg("com.android.settings")))

    // Scroll the settings to the top and find Notifications button
    var scrollableObj: UiObject2 = device.findObject(By.scrollable(true))
    scrollableObj.scrollUntil(Direction.UP, Until.scrollFinished(Direction.UP))
    val notificationsButton = scrollableObj.findObject(By.text("Notifications"))

    // Click the Notifications button and wait until a new window is opened.
    device.performActionAndWait({ notificationsButton.click() }, Until.newWindow(), 1000)
    scrollableObj = device.findObject(By.scrollable(true))
    // Scroll down until it finds a Do Not Disturb button.
    val doNotDisturb = scrollableObj.scrollUntil(
        Direction.DOWN,
        Until.findObject(By.textContains("Do Not Disturb"))
    )
    device.performActionAndWait({ doNotDisturb.click() }, Until.newWindow(), 1000)
    // Turn off the Do Not Disturb.
    val turnOnDoNotDisturb = device.findObject(By.text("Turn on now"))
    turnOnDoNotDisturb?.click()
    Assert.assertTrue(device.wait(Until.hasObject(By.text("Turn off now")), 1000))
}

Java


@Test
@SdkSuppress(minSdkVersion = 21)
public void turnOffDoNotDisturb() throws Exception{
    device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
    device.performActionAndWait(() -> {
        try {
            device.executeShellCommand("am start -a android.settings.SETTINGS");
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }, Until.newWindow(), 1000);
    // Check system settings has been opened.
    assertTrue(device.hasObject(By.pkg("com.android.settings")));

    // Scroll the settings to the top and find Notifications button
    UiObject2 scrollableObj = device.findObject(By.scrollable(true));
    scrollableObj.scrollUntil(Direction.UP, Until.scrollFinished(Direction.UP));
    UiObject2 notificationsButton = scrollableObj.findObject(By.text("Notifications"));

    // Click the Notifications button and wait until a new window is opened.
    device.performActionAndWait(() -> notificationsButton.click(), Until.newWindow(), 1000);
    scrollableObj = device.findObject(By.scrollable(true));
    // Scroll down until it finds a Do Not Disturb button.
    UiObject2 doNotDisturb = scrollableObj.scrollUntil(Direction.DOWN,
            Until.findObject(By.textContains("Do Not Disturb")));
    device.performActionAndWait(()-> doNotDisturb.click(), Until.newWindow(), 1000);
    // Turn off the Do Not Disturb.
    UiObject2 turnOnDoNotDisturb = device.findObject(By.text("Turn on now"));
    if(turnOnDoNotDisturb != null) {
        turnOnDoNotDisturb.click();
    }
    assertTrue(device.wait(Until.hasObject(By.text("Turn off now")), 1000));
}

Outros recursos

Para saber mais sobre o uso do UI Automator em testes do Android, consulte os recursos abaixo.

Documentação de referência:

Exemplos