Testar a UI de vários apps

Um teste de interface do usuário (IU) que envolve interações em vários apps permite verificar se o app se comporta corretamente quando o fluxo do usuário passa para outros apps ou para a IU do sistema. Um exemplo desse fluxo é um app de mensagens que permite ao usuário inserir uma mensagem de texto, inicia o seletor de contatos do Android para que o usuário selecione destinatários para o envio da mensagem e retorna o controle ao app original para o usuário enviar a mensagem.

Esta lição explica como programar esses testes de IU usando o framework de testes UI Automator oferecido pelo AndroidX Test. As APIs do UI Automator permitem que você interaja com elementos visíveis em um dispositivo, independentemente da Activity em foco. Seu teste pode procurar um componente de IU por meio de descritores convenientes, por exemplo, o texto exibido nesse componente ou a descrição do conteúdo relacionada. Os testes do UI Automator são executados em dispositivos com o Android 4.3 (API de nível 18) ou versão mais recente.

A estrutura de testes do UI Automator é uma API baseada em instrumentação e funciona com o executor de testes AndroidJUnitRunner.

Leia também a Referência de API do UI Automator e veja os Exemplos de código do UI Automator (em inglês).

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 para Android, é preciso definir uma referência de dependência para a biblioteca do UI Automator:

    dependencies {
        ...
        androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0'
    }
    

Para otimizar os testes do UI Automator, inspecione primeiro os componentes de IU do app de destino e verifique se é possível acessá-los. Essas dicas de otimização são descritas nas duas seções a seguir.

Inspecionar a IU em um dispositivo

Antes de criar seu teste, inspecione os componentes de IU visíveis no dispositivo. Para garantir que os testes do Automator de UI tenham acesso a esses componentes, verifique se esses componentes têm rótulos de texto visíveis, valores android:contentDescription ou ambos.

A ferramenta uiautomatorviewer disponibiliza uma interface visual conveniente para inspecionar a hierarquia de layouts e ver as propriedades de componentes de IU que estão visíveis no primeiro plano do dispositivo. Essas informações permitem criar testes mais detalhados por meio do UI Automator. Por exemplo, você pode criar um seletor de IU correspondente 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 uiautomatorviewer, clique no botão Device Screenshot.
  2. Passe o cursor do mouse 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 layouts no painel superior direito.
  3. Também é possível clicar no botão Toggle NAF Nodes para ver os componentes de IU a que o UI Automator não tem acesso. Somente informações limitadas podem ser disponibilizadas 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 apresenta um desempenho melhor nos apps que implementaram recursos de acessibilidade do Android. Quando você usa elementos de IU do tipo View ou uma subclasse de View do SDK, não é necessário implementar compatibilidade de acessibilidade, porque essas classes já fazem isso.

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

  1. Criar 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 ver outras orientações sobre como adicionar recursos de acessibilidade a elementos de visualização personalizados, consulte Criar visualizações personalizadas acessíveis. Para saber mais sobre práticas gerais recomendadas para acessibilidade no Android, consulte Como tornar apps mais acessíveis.

Criar uma classe de teste do UI Automator

Sua classe de testes do UI Automator deve ser programada da mesma maneira que uma classe de teste do JUnit 4. Para saber mais sobre a criação de classes de testes 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 é preciso especificar a classe AndroidJUnitRunner, que o AndroidX Test oferece como o executor de teste 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. Use um UiObject para acessar um componente de IU 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 executar nesse componente de IU, chamando um método UiObject. Por exemplo, chame performMultiPointerGesture() para simular um gesto de vários toques e setText() para editar um campo de texto. Você pode chamar as APIs nas etapas 2 e 3 várias vezes conforme necessário para testar interações mais complexas do usuário que envolvem diversos componentes de IU ou sequências de ações.
  4. Verifique se a IU reflete o estado ou o comportamento esperado quando essas interações são 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 os 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 executar ações no nível do dispositivo, como forçar o dispositivo a uma rotação específica, pressionar os botões D-pad e os botões "Início" e "Menu".

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

O snippet de código a seguir mostra como o teste pode usar uma instância de UiDevice e simular o pressionamento do botão "Início":

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 Android 4.3 (API de nível 18) ou versão mais recente, conforme exigido pelo framework do UI Automator.

Use o método findObject() para recuperar um UiObject que representa uma visualização correspondente a um determinado critério de seleção. É possível reutilizar as instâncias UiObject 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 UiObject para clicar em um elemento de IU ou consultar uma propriedade.

O snippet a seguir mostra como o teste pode criar instâncias UiObject que representem um botão "Cancelar" e um botão "OK" no app.

Kotlin

    val cancelButton: UiObject = device.findObject(
            UiSelector().text("Cancel").className("android.widget.Button")
    )
    val okButton: UiObject = device.findObject(
            UiSelector().text("OK").className("android.widget.Button")
    )

    // Simulate a user-click on the OK button, if found.
    if (okButton.exists() && okButton.isEnabled) {
        okButton.click()
    }
    

Java

    UiObject cancelButton = device.findObject(new UiSelector()
            .text("Cancel")
            .className("android.widget.Button"));
    UiObject okButton = device.findObject(new UiSelector()
            .text("OK")
            .className("android.widget.Button"));

    // Simulate a user-click on the OK button, if found.
    if(okButton.exists() && okButton.isEnabled()) {
        okButton.click();
    }
    

Especificar um seletor

Para acessar um componente de IU específico em um app, use a classe UiSelector. Essa classe representa uma consulta para elementos específicos na IU exibida no momento.

Se mais de um elemento correspondente for encontrado, o primeiro na hierarquia de layout será retornado como o destino UiObject. Ao criar um UiSelector, você pode encadear várias propriedades para refinar a pesquisa. Se nenhum elemento de IU correspondente for encontrado, uma UiAutomatorObjectNotFoundException será gerada.

Você pode usar o método childSelector() para aninhar várias instâncias UiSelector. O exemplo de código a seguir mostra como o teste pode especificar uma pesquisa para encontrar a primeira ListView na IU exibida no momento, e em seguida, procurar nessa ListView um elemento de IU com a propriedade de texto "Apps".

Kotlin

    val appItem: UiObject = device.findObject(
            UiSelector().className("android.widget.ListView")
                    .instance(0)
                    .childSelector(
                            UiSelector().text("Apps")
                    )
    )
    

Java

    UiObject appItem = device.findObject(new UiSelector()
            .className("android.widget.ListView")
            .instance(0)
            .childSelector(new UiSelector()
            .text("Apps")));
    

Ao especificar um seletor, use um código de recurso (se houver um atribuído a um elemento de UI) em vez de um elemento de texto ou descritor de conteúdo. Nem todos os elementos têm um elemento de texto (por exemplo, ícones em uma barra de ferramentas). Os seletores de texto são instáveis e podem causar falhas de teste quando há pequenas mudanças na IU. Eles também podem não ser dimensionados em diferentes idiomas e não corresponder às strings traduzidas.

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

Realizar ações

Depois que o teste tiver um objeto UiObject, você poderá chamar os métodos da classe UiObject para realizar interações do usuário no componente de IU representado por esse objeto. É possível especificar ações como:

  • click(): clica no centro dos limites visíveis do elemento de IU.
  • dragTo(): 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 clearTextField() limpa o texto existente em um campo editável.
  • swipeUp(): executa a ação de deslizar para cima no UiObject. Da mesma forma, os métodos swipeDown(), swipeLeft() e swipeRight() executam ações correspondentes.

A estrutura de testes do UI Automator permite enviar um Intent ou iniciar um Activity sem usar comandos do shell, recebendo um objeto Context por meio de getContext().

O snippet a seguir mostra como o teste pode usar um Intent para iniciar o app em teste. Essa abordagem é útil quando você tem interesse apenas 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);
    }
    

Realizar ações em conjuntos

Use a classe UiCollection para simular interações do usuário em um conjunto de itens, como músicas em um álbum ou uma lista de e-mails em uma caixa de entrada. Para criar um objeto UiCollection, especifique um UiSelector que pesquise um contêiner de IU ou um wrapper de outros elementos de IU filhos, como uma visualização de layout contém elementos de IU filhos.

O snippet de código a seguir mostra como seu teste pode criar um UiCollection para representar um álbum de vídeo exibido em um FrameLayout.

Kotlin

    val videos = UiCollection(UiSelector().className("android.widget.FrameLayout"))

    // Retrieve the number of videos in this collection:
    val count = videos.getChildCount(
            UiSelector().className("android.widget.LinearLayout")
    )

    // Find a specific video and simulate a user-click on it
    val video: UiObject = videos.getChildByText(
            UiSelector().className("android.widget.LinearLayout"),
            "Cute Baby Laughing"
    )
    video.click()

    // Simulate selecting a checkbox that is associated with the video
    val checkBox: UiObject = video.getChild(
            UiSelector().className("android.widget.Checkbox")
    )
    if (!checkBox.isSelected) checkBox.click()
    

Java

    UiCollection videos = new UiCollection(new UiSelector()
            .className("android.widget.FrameLayout"));

    // Retrieve the number of videos in this collection:
    int count = videos.getChildCount(new UiSelector()
            .className("android.widget.LinearLayout"));

    // Find a specific video and simulate a user-click on it
    UiObject video = videos.getChildByText(new UiSelector()
            .className("android.widget.LinearLayout"), "Cute Baby Laughing");
    video.click();

    // Simulate selecting a checkbox that is associated with the video
    UiObject checkBox = video.getChild(new UiSelector()
            .className("android.widget.Checkbox"));
    if(!checkBox.isSelected()) checkbox.click();
    

Realizar ações em visualizações roláveis

Use a classe UiScrollable para simular rolagens verticais ou horizontais em uma tela. Essa técnica é útil quando um elemento de IU está posicionado fora da tela e você precisa rolar para exibi-lo.

O snippet de código a seguir mostra como simular rolagens para baixo no menu de configurações e cliques em uma opção "Sobre o tablet":

Kotlin

    val settingsItem = UiScrollable(UiSelector().className("android.widget.ListView"))
    val about: UiObject = settingsItem.getChildByText(
            UiSelector().className("android.widget.LinearLayout"),
            "About tablet"
    )
    about.click()
    

Java

    UiScrollable settingsItem = new UiScrollable(new UiSelector()
            .className("android.widget.ListView"));
    UiObject about = settingsItem.getChildByText(new UiSelector()
            .className("android.widget.LinearLayout"), "About tablet");
    about.click();
    

Verificar resultados

O InstrumentationTestCase estende TestCase para que você possa usar os métodos Assert JUnit padrão para testar se os componentes de 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(UiSelector().packageName(CALC_PACKAGE).resourceId("two")).click()
        device.findObject(UiSelector().packageName(CALC_PACKAGE).resourceId("plus")).click()
        device.findObject(UiSelector().packageName(CALC_PACKAGE).resourceId("three")).click()
        device.findObject(UiSelector().packageName(CALC_PACKAGE).resourceId("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(new UiSelector()
                .packageName(CALC_PACKAGE).resourceId("two")).click();
        device.findObject(new UiSelector()
                .packageName(CALC_PACKAGE).resourceId("plus")).click();
        device.findObject(new UiSelector()
                .packageName(CALC_PACKAGE).resourceId("three")).click();
        device.findObject(new UiSelector()
                .packageName(CALC_PACKAGE).resourceId("equals")).click();

        // Verify the result = 5
        UiObject 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.

Outros recursos

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

Amostras

  • BasicSample (em inglês): amostra básica do UI Automator.

Codelabs