The Android Developer Challenge is back! Submit your idea before December 2.

UI de teste para um único app

O teste das interações do usuário em um único app ajuda a garantir que os usuários não encontrem resultados inesperados ou tenham uma experiência ruim ao usar seu app. Crie o hábito de programar testes de interface do usuário (IU) quando precisar verificar se a IU do seu app está funcionando corretamente.

O framework de testes Espresso, disponibilizado pelo AndroidX Test, oferece APIs para programar testes de IU que simulam interações do usuário em um único app de destino. Os testes do Espresso podem ser executados em dispositivos com o Android 2.3.3 (API nível 10) e posterior. Um dos principais benefícios do uso do Espresso é que ele oferece sincronização automática das ações de teste com a IU do app que você está testando. O Espresso detecta quando a linha de execução principal está inativa. Assim, é possível executar seus comandos de teste no momento apropriado, melhorando a confiabilidade dos testes. Esse recurso também dispensa a necessidade de adicionar soluções alternativas de tempo, como Thread.sleep() no código de teste.

O framework de testes Espresso é uma API baseada em instrumentação e funciona com o executor de testes AndroidJUnitRunner.

Configurar o Espresso

Antes de criar seu teste de IU com o Espresso, defina uma referência de dependência para a biblioteca do Espresso:

    dependencies {
        androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0'
    }
    

Desative as animações no dispositivo de teste. Se as animações do sistema estiverem ativadas, poderão ocorrer resultados inesperados ou a falha do teste. Para desativar as animações, acesse Config., abra as Opções do desenvolvedor e desative todos os itens a seguir:

  • Escala de animação da janela
  • Escala de animação de transição
  • Escala de duração do Animator

Se você quer configurar o projeto para usar os recursos do Espresso, além daqueles oferecidos pela API principal, consulte os guias específicos do Espresso.

Criar uma classe de teste do Espresso

Para criar um teste do Espresso, siga este modelo de programação:

  1. Localize o componente de IU que você quer testar em uma Activity (por exemplo, um botão de login no app). Para isso, chame os métodos onView() ou onData() para os controles da AdapterView.
  2. Simule uma interação específica do usuário a ser realizada nesse componente de IU. Para isso, chame os métodos ViewInteraction.perform() ou DataInteraction.perform() e passe a ação do usuário (por exemplo, clicar no botão de login). Para sequenciar várias ações no mesmo componente de IU, encadeie-as usando uma lista separada por vírgulas no argumento do método.
  3. Repita as etapas acima conforme necessário para simular um fluxo de usuários em várias atividades no app de destino.
  4. Use os métodos ViewAssertions para verificar se a IU reflete o estado ou o comportamento esperado quando essas interações são realizadas.

Essas etapas são discutidas com mais detalhes nas seções a seguir.

O snippet de código a seguir mostra como sua classe de teste pode invocar esse fluxo de trabalho básico:

Kotlin

    onView(withId(R.id.my_view))            // withId(R.id.my_view) is a ViewMatcher
            .perform(click())               // click() is a ViewAction
            .check(matches(isDisplayed()))  // matches(isDisplayed()) is a ViewAssertion
    

Java

    onView(withId(R.id.my_view))            // withId(R.id.my_view) is a ViewMatcher
            .perform(click())               // click() is a ViewAction
            .check(matches(isDisplayed())); // matches(isDisplayed()) is a ViewAssertion
    

Usar o Espresso com ActivityTestRule

A seção a seguir descreve como criar um novo teste do Espresso no estilo JUnit 4 e usar o ActivityTestRule para reduzir a quantidade de código clichê necessária. Usando ActivityTestRule, o framework de testes inicia a atividade em teste antes de cada método de teste anotado com @Test e antes de qualquer método anotado com @Before. O framework lida com o encerramento da atividade depois da conclusão do teste, e todos os métodos anotados com @After são executados.

Kotlin

    package com.example.android.testing.espresso.BasicSample

    import org.junit.Before
    import org.junit.Rule
    import org.junit.Test
    import org.junit.runner.RunWith

    import androidx.test.rule.ActivityTestRule
    import androidx.test.runner.AndroidJUnit4

    @RunWith(AndroidJUnit4::class)
    @LargeTest
    class ChangeTextBehaviorTest {

        private lateinit var stringToBetyped: String

        @get:Rule
        var activityRule: ActivityTestRule<MainActivity>
                = ActivityTestRule(MainActivity::class.java)

        @Before
        fun initValidString() {
            // Specify a valid string.
            stringToBetyped = "Espresso"
        }

        @Test
        fun changeText_sameActivity() {
            // Type text and then press the button.
            onView(withId(R.id.editTextUserInput))
                    .perform(typeText(stringToBetyped), closeSoftKeyboard())
            onView(withId(R.id.changeTextBt)).perform(click())

            // Check that the text was changed.
            onView(withId(R.id.textToBeChanged))
                    .check(matches(withText(stringToBetyped)))
        }
    }
    

Java

    package com.example.android.testing.espresso.BasicSample;

    import org.junit.Before;
    import org.junit.Rule;
    import org.junit.Test;
    import org.junit.runner.RunWith;

    import androidx.test.rule.ActivityTestRule;
    import androidx.test.runner.AndroidJUnit4;

    @RunWith(AndroidJUnit4.class)
    @LargeTest
    public class ChangeTextBehaviorTest {

        private String stringToBetyped;

        @Rule
        public ActivityTestRule<MainActivity> activityRule
                = new ActivityTestRule<>(MainActivity.class);

        @Before
        public void initValidString() {
            // Specify a valid string.
            stringToBetyped = "Espresso";
        }

        @Test
        public void changeText_sameActivity() {
            // Type text and then press the button.
            onView(withId(R.id.editTextUserInput))
                    .perform(typeText(stringToBetyped), closeSoftKeyboard());
            onView(withId(R.id.changeTextBt)).perform(click());

            // Check that the text was changed.
            onView(withId(R.id.textToBeChanged))
                    .check(matches(withText(stringToBetyped)));
        }
    }
    

Acessar componentes de IU

Antes que o Espresso possa interagir com o app em teste, é necessário especificar o componente ou a visualização de IU. O Espresso é compatível com uso de matchers do Hamcrest (em inglês) para especificar visualizações e adaptadores no app.

Para encontrar a visualização, chame o método onView() e passe um matcher de visualização que especifique aquela que você quer usar. Esse processo é descrito em mais detalhes em Especificar um matcher de visualização. O método onView() retorna um objeto ViewInteraction que permite que seu teste interaja com a visualização. No entanto, a chamada do método onView() pode não funcionar se você quer localizar uma visualização em um layout RecyclerView. Nesse caso, siga as instruções em Localizar uma visualização em uma AdapterView.

Observação: o método onView() não verifica se a visualização que você especificou é válida. Em vez disso, o Espresso pesquisa apenas a hierarquia de visualizações atual por meio do matcher informado. Se nenhuma correspondência for encontrada, o método gerará uma NoMatchingViewException.

O snippet de código a seguir mostra como programar um teste que acessa um campo EditText, insere uma string de texto, fecha o teclado virtual e executa um clique de botão.

Kotlin

    fun testChangeText_sameActivity() {
        // Type text and then press the button.
        onView(withId(R.id.editTextUserInput))
                .perform(typeText(STRING_TO_BE_TYPED), closeSoftKeyboard())
        onView(withId(R.id.changeTextButton)).perform(click())

        // Check that the text was changed.
        ...
    }
    

Java

    public void testChangeText_sameActivity() {
        // Type text and then press the button.
        onView(withId(R.id.editTextUserInput))
                .perform(typeText(STRING_TO_BE_TYPED), closeSoftKeyboard());
        onView(withId(R.id.changeTextButton)).perform(click());

        // Check that the text was changed.
        ...
    }
    

Especificar um matcher de visualização

Especifique um matcher de visualização usando estas abordagens:

  • Chamar métodos na classe ViewMatchers. Por exemplo, para localizar uma visualização procurando uma string de texto que ela exibe, você pode chamar um método como este:

    Kotlin

        onView(withText("Sign-in"))
        

    Java

        onView(withText("Sign-in"));
        

    Da mesma forma, você pode chamar withId() e informar o código de recurso (R.id) da visualização, conforme mostrado neste exemplo:

    Kotlin

        onView(withId(R.id.button_signin))
        

    Java

        onView(withId(R.id.button_signin));
        

    Não há garantia de que os códigos de recursos do Android sejam exclusivos. Se o teste tentar corresponder a um código de recurso usado por mais de uma visualização, o Espresso gerará uma AmbiguousViewMatcherException.

  • Usar a classe Matchers do Hamcrest. Você pode usar os métodos allOf() para combinar vários matchers, como containsString() e instanceOf(). Essa abordagem permite filtrar os resultados de correspondência de maneira mais restrita, conforme mostrado neste exemplo:

    Kotlin

        onView(allOf(withId(R.id.button_signin), withText("Sign-in")))
        

    Java

        onView(allOf(withId(R.id.button_signin), withText("Sign-in")));
        

    Você pode usar a palavra-chave not para filtrar visualizações que não correspondam ao matcher, conforme mostrado neste exemplo:

    Kotlin

        onView(allOf(withId(R.id.button_signin), not(withText("Sign-out"))))
        

    Java

        onView(allOf(withId(R.id.button_signin), not(withText("Sign-out"))));
        

    Para usar esses métodos no seu teste, importe o pacote org.hamcrest.Matchers. Para saber mais sobre a correspondência do Hamcrest, consulte o site do Hamcrest (em inglês).

Para melhorar o desempenho dos testes do Espresso, especifique as informações mínimas de correspondência necessárias para localizar a visualização de destino. Por exemplo, se uma visualização puder ser identificada exclusivamente pelo texto descritivo dela, não será necessário especificar que ela também pode ser atribuída na instância da TextView.

Localizar uma visualização em uma AdapterView

Em um widget AdapterView, a visualização é preenchida dinamicamente com visualizações filhas durante a execução. Se a visualização de destino que você quer testar estiver dentro de uma AdapterView (como uma ListView, GridView ou Spinner), o método onView() não funcionará, porque apenas um subconjunto das visualizações pode ser carregado na hierarquia atual.

Em vez disso, chame o método onData() para usar um objeto DataInteraction e acessar o elemento de visualização de destino. O Espresso processa o carregamento do elemento de visualização de destino na hierarquia atual. O Espresso também é responsável pela rolagem até o elemento de destino e por colocar o elemento em foco.

Observação: o método onData() não verifica se o item especificado corresponde a uma visualização. O Espresso pesquisa apenas a hierarquia de visualizações atual. Se nenhuma correspondência for encontrada, o método gerará uma NoMatchingViewException.

O snippet de código a seguir mostra como usar o método onData() junto com a correspondência do Hamcrest para procurar uma linha específica em uma lista que contém uma determinada string. Neste exemplo, a classe LongListActivity contém uma lista de strings expostas por meio de um SimpleAdapter.

Kotlin

    onData(allOf(`is`(instanceOf(Map::class.java)),
            hasEntry(equalTo(LongListActivity.ROW_TEXT),
            `is`("test input"))))
    

Java

    onData(allOf(is(instanceOf(Map.class)),
            hasEntry(equalTo(LongListActivity.ROW_TEXT), is("test input"))));
    

Realizar ações

Chame os métodos ViewInteraction.perform() ou DataInteraction.perform() para simular interações do usuário no componente da IU. É preciso passar um ou mais objetos ViewAction como argumentos. O Espresso dispara cada ação em sequência, de acordo com a ordem especificada, e as executa na linha de execução principal.

A classe ViewActions oferece uma lista de métodos auxiliares para especificar ações comuns. Você pode usar esses métodos como atalhos convenientes, em vez de criar e configurar objetos ViewAction individuais. É possível especificar ações como:

Se a visualização de destino estiver dentro de uma ScrollView, realize a ação ViewActions.scrollTo() primeiro para exibir a visualização na tela antes de prosseguir com outras ações. A ação ViewActions.scrollTo() não terá efeito se a visualização já estiver sendo exibida.

Testar atividades isoladamente com o Espresso Intents

O Espresso Intents permite validar e criar stubs de intents enviados por um app. Com o Espresso Intents, você pode testar um app, atividade ou serviço isoladamente. Para isso, intercepte os intents de saída, crie um stub do resultado e envie-o de volta ao componente em teste.

Para começar a testar com o Espresso Intents, é necessário adicionar a seguinte linha ao arquivo build.gradle do app:

    dependencies {
      androidTestImplementation 'androidx.test.espresso:espresso-intents:3.1.0'
    }
    

Para testar um intent, é preciso criar uma instância da classe IntentsTestRule, que é muito semelhante à classe ActivityTestRule. A classe IntentsTestRule inicializa o Espresso Intents antes de cada teste, encerra a atividade do host e libera o Espresso Intents depois de cada teste.

A classe de teste mostrada no snippet de código a seguir oferece um teste simples para um intent explícito. Ela testa as atividades e os intents criados no tutorial Criar seu primeiro app.

Kotlin

    private const val MESSAGE = "This is a test"
    private const val PACKAGE_NAME = "com.example.myfirstapp"

    @RunWith(AndroidJUnit4::class)
    class SimpleIntentTest {

        /* Instantiate an IntentsTestRule object. */
        @get:Rule
        var intentsRule: IntentsTestRule<MainActivity> = IntentsTestRule(MainActivity::class.java)

        @Test
        fun verifyMessageSentToMessageActivity() {

            // Types a message into a EditText element.
            onView(withId(R.id.edit_message))
                    .perform(typeText(MESSAGE), closeSoftKeyboard())

            // Clicks a button to send the message to another
            // activity through an explicit intent.
            onView(withId(R.id.send_message)).perform(click())

            // Verifies that the DisplayMessageActivity received an intent
            // with the correct package name and message.
            intended(allOf(
                    hasComponent(hasShortClassName(".DisplayMessageActivity")),
                    toPackage(PACKAGE_NAME),
                    hasExtra(MainActivity.EXTRA_MESSAGE, MESSAGE)))

        }
    }
    

Java

    @Large
    @RunWith(AndroidJUnit4.class)
    public class SimpleIntentTest {

        private static final String MESSAGE = "This is a test";
        private static final String PACKAGE_NAME = "com.example.myfirstapp";

        /* Instantiate an IntentsTestRule object. */
        @Rule
        public IntentsTestRule<MainActivity> intentsRule =
                new IntentsTestRule<>(MainActivity.class);

        @Test
        public void verifyMessageSentToMessageActivity() {

            // Types a message into a EditText element.
            onView(withId(R.id.edit_message))
                    .perform(typeText(MESSAGE), closeSoftKeyboard());

            // Clicks a button to send the message to another
            // activity through an explicit intent.
            onView(withId(R.id.send_message)).perform(click());

            // Verifies that the DisplayMessageActivity received an intent
            // with the correct package name and message.
            intended(allOf(
                    hasComponent(hasShortClassName(".DisplayMessageActivity")),
                    toPackage(PACKAGE_NAME),
                    hasExtra(MainActivity.EXTRA_MESSAGE, MESSAGE)));

        }
    }
    

Para saber mais sobre o Espresso Intents, consulte a documentação do Espresso Intents no site de AndroidX Text. Você também pode fazer o download das amostras de código IntentsBasicSample e IntentsAdvancedSample (ambos em inglês).

Testar WebViews com o Espresso Web

O Espresso Web permite testar os componentes WebView de uma atividade. Ele usa a API WebDriver (em inglês) para inspecionar e controlar o comportamento de uma WebView.

Para começar a testar com o Espresso Web, é necessário adicionar a seguinte linha ao arquivo build.gradle do app:

    dependencies {
      androidTestImplementation 'androidx.test.espresso:espresso-web:3.1.0'
    }
    

Quando você cria um teste usando o Espresso Web, é preciso ativar o JavaScript na WebView ao instanciar o objeto ActivityTestRule para testar a atividade. Nos testes, você pode selecionar os elementos HTML exibidos na WebView e simular interações do usuário, como inserir texto em uma caixa de texto e clicar em um botão. Quando as ações forem concluídas, você poderá verificar se os resultados na página da Web correspondem aos resultados esperados.

No snippet de código a seguir, a classe testa um componente WebView com o valor de código "webview" na atividade em teste. O teste typeTextInInput_clickButton_SubmitsForm() seleciona um elemento <input> na página da Web, insere um texto e verifica o texto que aparece em outro elemento.

Kotlin

    private const val MACCHIATO = "Macchiato"
    private const val DOPPIO = "Doppio"

    @LargeTest
    @RunWith(AndroidJUnit4::class)
    class WebViewActivityTest {

        @get:Rule
        val activityRule = object : ActivityTestRule<WebViewActivity>(
                WebViewActivity::class.java,
                false,      /* Initial touch mode */
                false       /* launch activity */
        ) {
            override fun afterActivityLaunched() {
                // Enable JavaScript.
                onWebView().forceJavascriptEnabled()
            }
        }

        @Test
        fun typeTextInInput_clickButton_SubmitsForm() {
            // Lazily launch the Activity with a custom start Intent per test
            activityRule.launchActivity(withWebFormIntent())

            // Selects the WebView in your layout.
            // If you have multiple WebViews you can also use a
            // matcher to select a given WebView, onWebView(withId(R.id.web_view)).
            onWebView()
                    // Find the input element by ID
                    .withElement(findElement(Locator.ID, "text_input"))
                    // Clear previous input
                    .perform(clearElement())
                    // Enter text into the input element
                    .perform(DriverAtoms.webKeys(MACCHIATO))
                    // Find the submit button
                    .withElement(findElement(Locator.ID, "submitBtn"))
                    // Simulate a click via JavaScript
                    .perform(webClick())
                    // Find the response element by ID
                    .withElement(findElement(Locator.ID, "response"))
                    // Verify that the response page contains the entered text
                    .check(webMatches(getText(), containsString(MACCHIATO)))
        }
    }
    

Java

    @LargeTest
    @RunWith(AndroidJUnit4.class)
    public class WebViewActivityTest {

        private static final String MACCHIATO = "Macchiato";
        private static final String DOPPIO = "Doppio";

        @Rule
        public ActivityTestRule<WebViewActivity> activityRule =
            new ActivityTestRule<WebViewActivity>(WebViewActivity.class,
                false /* Initial touch mode */, false /*  launch activity */) {

            @Override
            protected void afterActivityLaunched() {
                // Enable JavaScript.
                onWebView().forceJavascriptEnabled();
            }
        }

        @Test
        public void typeTextInInput_clickButton_SubmitsForm() {
           // Lazily launch the Activity with a custom start Intent per test
           activityRule.launchActivity(withWebFormIntent());

           // Selects the WebView in your layout.
           // If you have multiple WebViews you can also use a
           // matcher to select a given WebView, onWebView(withId(R.id.web_view)).
           onWebView()
               // Find the input element by ID
               .withElement(findElement(Locator.ID, "text_input"))
               // Clear previous input
               .perform(clearElement())
               // Enter text into the input element
               .perform(DriverAtoms.webKeys(MACCHIATO))
               // Find the submit button
               .withElement(findElement(Locator.ID, "submitBtn"))
               // Simulate a click via JavaScript
               .perform(webClick())
               // Find the response element by ID
               .withElement(findElement(Locator.ID, "response"))
               // Verify that the response page contains the entered text
               .check(webMatches(getText(), containsString(MACCHIATO)));
        }
    }
    

Para saber mais sobre o Espresso Web, consulte a documentação do Espresso Web no site do AndroidX Text.. Você também pode fazer o download desse snippet de código como parte da amostra de código do Espresso Web (em inglês).

Verificar resultados

Chame os métodos ViewInteraction.check() ou DataInteraction.check() para declarar que a visualização na IU corresponde a algum estado esperado. É preciso passar um objeto ViewAssertion como argumento. Se a declaração falhar, o Espresso gerará uma AssertionFailedError.

A classe ViewAssertions disponibiliza uma lista de métodos auxiliares para especificar declarações comuns. As declarações que você pode usar incluem:

  • doesNotExist: declara que não há visualização correspondente aos critérios especificados na hierarquia atual.
  • matches: declara que a visualização especificada existe na hierarquia atual e que o estado dela corresponde a algum matcher do Hamcrest.
  • selectedDescendentsMatch: declara que as visualizações filhas especificadas para uma visualização mãe existem e que o estado delas corresponde a algum matcher do Hamcrest.

O snippet de código a seguir mostra como verificar se o texto exibido na IU tem o mesmo valor que o texto digitado anteriormente no campo EditText.

Kotlin

    fun testChangeText_sameActivity() {
        // Type text and then press the button.
        ...

        // Check that the text was changed.
        onView(withId(R.id.textToBeChanged))
                .check(matches(withText(STRING_TO_BE_TYPED)))
    }
    

Java

    public void testChangeText_sameActivity() {
        // Type text and then press the button.
        ...

        // Check that the text was changed.
        onView(withId(R.id.textToBeChanged))
                .check(matches(withText(STRING_TO_BE_TYPED)));
    }
    

Executar testes do Espresso em um dispositivo ou emulador

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

Para executar o teste do Espresso, siga as etapas para executar testes de instrumentação descritos em Primeiros passos sobre testes.

Leia também a Referência de API do Espresso.

Outros recursos

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

Amostras

Codelabs