Conceitos básicos do Espresso

Este documento explica como concluir tarefas de teste comuns e automatizadas por meio da API do Espresso.

A API do Espresso incentiva os autores de testes a pensar o que o usuário pode fazer enquanto interage com o aplicativo, localizando elementos de UI e interagindo com eles. Ao mesmo tempo, o framework evita o acesso direto às atividades e visualizações do app, porque a retenção desses objetos e a operação deles fora da linha de execução de IU é uma das principais fontes de falhas nos testes. Portanto, você não verá métodos como getView() e getCurrentActivity() na API do Espresso. Você ainda pode trabalhar com segurança em visualizações, implementando suas próprias subclasses de ViewAction e ViewAssertion.

Componentes da API

Os principais componentes do Espresso incluem:

  • Espresso: ponto de entrada para interações com visualizações (via onView() e onData()). Também expõe APIs que não estão necessariamente vinculadas a nenhuma visualização, como pressBack().
  • ViewMatchers: um conjunto de objetos que implementam a interface Matcher<? super View>. Você pode transmitir um ou mais deles ao método onView() para localizar uma visualização na hierarquia atual.
  • ViewActions: um conjunto de objetos ViewAction que podem ser transmitidos para o método ViewInteraction.perform(), como click().
  • ViewAssertions: um conjunto de objetos ViewAssertion que podem ser transmitidos ao método ViewInteraction.check(). Na maioria das vezes, você usará a declaração de correspondências, que usa um matcher de visualização para declarar o estado da visualização selecionada no momento.

Por exemplo:

Kotlin

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

Java

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

Encontrar uma visualização

Na grande maioria dos casos, o método onView() usa um matcher de Hamcrest que precisa corresponder a uma, e apenas uma, visualização na hierarquia atual. Os matchers são um recurso avançado. Se você já os usou com o Mockito ou o JUnit, já sabe como eles funcionam. Caso não conheça os matchers do Hamcrest, sugerimos que comece com uma leitura rápida desta apresentação (em inglês).

Em geral, a visualização desejada tem um R.id exclusivo, e um matcher withId simples reduz a pesquisa de visualização. No entanto, existem muitos casos legítimos em que não é possível determinar o R.id no momento do desenvolvimento do teste. Por exemplo, a visualização específica pode não ter um R.id, ou o R.id não é exclusivo. Isso pode tornar a programação de testes normais instável e complicada, porque a maneira comum de acessar a visualização, com findViewById(), não funciona. Portanto, pode ser necessário acessar membros privados da atividade ou do fragmento que contém a visualização ou um contêiner com um R.id conhecido e navegar até o conteúdo dessa visualização específica.

O Espresso lida com esse problema de maneira limpa, permitindo restringir a visualização por meio de objetos ViewMatcher já existentes ou personalizados.

Encontrar uma visualização pelo R.id é tão simples quanto chamar onView():

Kotlin

    onView(withId(R.id.my_view))
    

Java

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

Às vezes, os valores R.id são compartilhados entre várias visualizações. Quando isso acontece, uma tentativa de usar um R.id específico gera uma exceção, como AmbiguousViewMatcherException. A mensagem de exceção oferece uma representação em texto da hierarquia atual, em que você pode pesquisar e localizar as visualizações que correspondem ao R.id não exclusivo:

    java.lang.RuntimeException:
    androidx.test.espresso.AmbiguousViewMatcherException
    This matcher matches multiple views in the hierarchy: (withId: is <123456789>)

    ...

    +----->SomeView{id=123456789, res-name=plus_one_standard_ann_button,
    visibility=VISIBLE, width=523, height=48, has-focus=false, has-focusable=true,
    window-focus=true, is-focused=false, is-focusable=false, enabled=true,
    selected=false, is-layout-requested=false, text=,
    root-is-layout-requested=false, x=0.0, y=625.0, child-count=1}
    ****MATCHES****
    |
    +------>OtherView{id=123456789, res-name=plus_one_standard_ann_button,
    visibility=VISIBLE, width=523, height=48, has-focus=false, has-focusable=true,
    window-focus=true, is-focused=false, is-focusable=true, enabled=true,
    selected=false, is-layout-requested=false, text=Hello!,
    root-is-layout-requested=false, x=0.0, y=0.0, child-count=1}
    ****MATCHES****
    

Analisando os vários atributos das visualizações, você pode encontrar propriedades de identificação exclusiva. No exemplo acima, uma das visualizações tem o texto "Hello!". Você pode usá-lo para restringir sua pesquisa por meio de matchers de combinação:

Kotlin

    onView(allOf(withId(R.id.my_view), withText("Hello!")))
    

Java

    onView(allOf(withId(R.id.my_view), withText("Hello!")));
    

Você também pode optar por não reverter nenhum dos matchers:

Kotlin

    onView(allOf(withId(R.id.my_view), not(withText("Unwanted"))))
    

Java

    onView(allOf(withId(R.id.my_view), not(withText("Unwanted"))));
    

Consulte ViewMatchers para ver os matchers de visualização fornecidos pelo Espresso.

Considerações

  • Em um app com comportamento adequado, todas as visualizações com que o usuário pode interagir devem conter texto descritivo ou uma descrição do conteúdo. Consulte Como tornar apps mais acessíveis para ver mais detalhes. Se não for possível restringir uma pesquisa usando withText() ou withContentDescription(), trate-a como um bug de acessibilidade.
  • Use o matcher menos descritivo que encontrar a visualização que você está procurando. Não especifique demais, porque isso forçará o framework a trabalhar mais do que o necessário. Por exemplo, se uma visualização tiver identificação exclusiva por meio do texto dela, você não precisará especificar que a visualização também pode ser atribuída na TextView. Para muitas visualizações, o R.id deve ser suficiente.
  • Se a visualização de destino está dentro de um AdapterView, por exemplo, ListView, GridView ou Spinner, o método onView() pode não funcionar. Nesses casos, use onData().

Realizar uma ação em uma visualização

Quando você encontra um matcher adequado para a visualização de destino, pode realizar instâncias de ViewAction nele por meio do método de execução.

Por exemplo, para clicar na visualização:

Kotlin

    onView(...).perform(click())
    

Java

    onView(...).perform(click());
    

Você pode executar uma ou mais ações com uma chamada de execução:

Kotlin

    onView(...).perform(typeText("Hello"), click())
    

Java

    onView(...).perform(typeText("Hello"), click());
    

Se a visualização com que você está trabalhando está dentro de uma ScrollView (vertical ou horizontal), anteceda as ações que exigem que a visualização seja exibida, por exemplo, click() e typeText(), com scrollTo(). Isso garante que a visualização seja exibida antes de dar continuidade à outra ação:

Kotlin

    onView(...).perform(scrollTo(), click())
    

Java

    onView(...).perform(scrollTo(), click());
    

Consulte ViewActions para ver as ações de visualização oferecidas pelo Espresso.

Verificar as declarações de visualização

As declarações podem ser aplicadas à visualização selecionada no momento, com o método check(). A declaração mais usada é a matches(). Ele usa um objeto ViewMatcher para declarar o estado da visualização selecionada no momento.

Por exemplo, para verificar se uma visualização tem o texto "Hello!":

Kotlin

    onView(...).check(matches(withText("Hello!")))
    

Java

    onView(...).check(matches(withText("Hello!")));
    

Se quiser declarar que "Hello!" é o conteúdo da visualização, a seguinte prática é recomendada:

Kotlin

    // Don't use assertions like withText inside onView.
    onView(allOf(withId(...), withText("Hello!"))).check(matches(isDisplayed()))
    

Java

    // Don't use assertions like withText inside onView.
    onView(allOf(withId(...), withText("Hello!"))).check(matches(isDisplayed()));
    

Por outro lado, se quiser declarar que uma visualização com o texto "Hello!" está visível, por exemplo, depois de mudar a sinalização da visibilidade de exibições, o código será aceito.

Teste simples para declaração de visualizações

Neste exemplo, SimpleActivity contém um Button e um TextView. Quando o botão é clicado, o conteúdo de TextView muda para "Hello Espresso!".

Veja como testar isso com o Espresso:

Clicar no botão

O primeiro passo é procurar uma propriedade que ajude a encontrar o botão. O botão na SimpleActivity tem um R.id exclusivo, como esperado.

Kotlin

    onView(withId(R.id.button_simple))
    

Java

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

Agora, para executar o clique:

Kotlin

    onView(withId(R.id.button_simple)).perform(click())
    

Java

    onView(withId(R.id.button_simple)).perform(click());
    

Verificar o texto de TextView

O TextView com o texto a ser verificado também tem um R.id exclusivo:

Kotlin

    onView(withId(R.id.text_simple))
    

Java

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

Agora, para verificar o texto do conteúdo:

Kotlin

    onView(withId(R.id.text_simple)).check(matches(withText("Hello Espresso!")))
    

Java

    onView(withId(R.id.text_simple)).check(matches(withText("Hello Espresso!")));
    

Verificar o carregamento de dados nas visualizações do adaptador

AdapterView é um tipo especial de widget que carrega os dados dinamicamente a partir de um adaptador. O exemplo mais comum de um AdapterView é ListView. Ao contrário dos widgets estáticos, por exemplo, LinearLayout, apenas um subconjunto dos elementos filhos AdapterView pode ser carregado na hierarquia atual. Uma pesquisa onView() simples não encontraria visualizações que não estão carregadas.

Para lidar com isso, o Espresso disponibiliza um ponto de entrada onData() separado, capaz de carregar primeiramente o item do adaptador em questão, colocando-o em foco antes de operar nele ou em qualquer um dos elementos filhos.

Aviso: as implementações personalizadas de AdapterView poderão apresentar problemas com o método onData() se quebrarem contratos de herança, principalmente a API getItem(). Nesses casos, a ação mais indicada é refatorar o código do aplicativo. Se a refatoração não for possível, implemente um AdapterViewProtocol personalizado correspondente. Para ver mais informações, consulte a classe AdapterViewProtocols padrão disponibilizada pelo Espresso.

Teste simples para visualização do adaptador

Este teste simples demonstra como usar onData(). SimpleActivity contém um Spinner com alguns itens que representam tipos de bebidas de café. Quando um item é selecionado, uma TextView muda para "One %s a day!", em que %s representa o item selecionado.

O objetivo deste teste é abrir o Spinner, selecionar um item específico e verificar se a TextView contém o item. Como a classe Spinner é baseada em AdapterView, é recomendado usar onData() em vez de onView() para relacionar o item.

Abrir a seleção do item

Kotlin

    onView(withId(R.id.spinner_simple)).perform(click())
    

Java

    onView(withId(R.id.spinner_simple)).perform(click());
    

Selecionar um item

Para a seleção do item, o Spinner cria um ListView com o próprio conteúdo. Essa visualização pode ser muito longa, e o elemento pode não ser contribuído para a hierarquia de visualização. Ao usar onData(), forçamos o elemento visado na hierarquia de visualização. Os itens em Spinner são strings. Portanto, vamos associar um item que seja igual à string "Americano":

Kotlin

    onData(allOf(`is`(instanceOf(String::class.java)),
            `is`("Americano"))).perform(click())
    

Java

    onData(allOf(is(instanceOf(String.class)), is("Americano"))).perform(click());
    

Verificar se o texto está correto

Kotlin

    onView(withId(R.id.spinnertext_simple))
        .check(matches(withText(containsString("Americano"))))
    

Java

    onView(withId(R.id.spinnertext_simple))
        .check(matches(withText(containsString("Americano"))));
    

Depurar

O Espresso oferece informações úteis sobre depuração quando um teste falha:

Gerar registros

O Expresso registra todas as ações de visualização no Logcat. Por exemplo:

    ViewInteraction: Performing 'single click' action on view with text: Espresso
    

Hierarquia de visualização

O Espresso imprime a hierarquia de visualização na mensagem de exceção quando onView() falha.

  • Se onView() não encontrar a visualização de destino, uma NoMatchingViewException será gerada. Você pode analisar a hierarquia de visualização na string da exceção para saber por que o matcher não associou nenhuma visualização.
  • Se onView() encontrar várias visualizações correspondentes ao matcher especificado, uma AmbiguousViewMatcherException será gerada. A hierarquia de visualização é impressa, e todas as visualizações correspondentes são marcadas com o rótulo MATCHES:
    java.lang.RuntimeException:
    androidx.test.espresso.AmbiguousViewMatcherException
    This matcher matches multiple views in the hierarchy: (withId: is <123456789>)

    ...

    +----->SomeView{id=123456789, res-name=plus_one_standard_ann_button,
    visibility=VISIBLE, width=523, height=48, has-focus=false, has-focusable=true,
    window-focus=true, is-focused=false, is-focusable=false, enabled=true,
    selected=false, is-layout-requested=false, text=,
    root-is-layout-requested=false, x=0.0, y=625.0, child-count=1}
    ****MATCHES****
    |
    +------>OtherView{id=123456789, res-name=plus_one_standard_ann_button,
    visibility=VISIBLE, width=523, height=48, has-focus=false, has-focusable=true,
    window-focus=true, is-focused=false, is-focusable=true, enabled=true,
    selected=false, is-layout-requested=false, text=Hello!,
    root-is-layout-requested=false, x=0.0, y=0.0, child-count=1}
    ****MATCHES****
    

Ao lidar com uma hierarquia de visualização complicada ou um comportamento inesperado dos widgets, é sempre útil usar o Hierarchy Viewer no Android Studio para ver uma explicação.

Avisos de visualização do adaptador

O Espresso avisa os usuários sobre a presença de widgets AdapterView. Quando uma operação onView() gera uma NoMatchingViewException e widgets AdapterView na hierarquia de visualização, a solução mais comum é usar onData(). A mensagem da exceção incluirá um aviso com uma lista das visualizações do adaptador. Você pode usar essas informações para invocar onData() para carregar a visualização de destino.

Outros recursos

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

Amostras