Receitas do Espresso

Este documento descreve como configurar diversos testes comuns do Espresso.

Associar uma visualização ao lado de outra

Um layout pode conter certas visualizações que não são exclusivas por si só. Por exemplo, um botão de chamada repetida em uma tabela de contatos pode ter o mesmo R.id, conter o mesmo texto e ter as mesmas propriedades de outros botões de chamada na hierarquia de visualização.

Por exemplo, nesta atividade, a visualização com o texto "7" se repete em várias linhas:

Uma atividade de lista mostrando três cópias do mesmo elemento de visualização dentro de uma lista de três itens

Geralmente, a visualização não exclusiva é pareada com uma etiqueta exclusiva localizada ao lado dela, como o nome de um contato ao lado do botão de chamada. Nesse caso, você pode usar o correspondente hasSibling() para restringir sua seleção:

Kotlin

    onView(allOf(withText("7"), hasSibling(withText("item: 0"))))
        .perform(click())
    

Java

    onView(allOf(withText("7"), hasSibling(withText("item: 0"))))
        .perform(click());
    

Associar uma visualização que está dentro de uma barra de ações

O ActionBarTestActivity tem duas barras de ações diferentes: uma normal e uma contextual, criada a partir de um menu de opções. Ambas as barras de ações têm um item que fica sempre visível e dois itens que ficam visíveis apenas no menu flutuante. Quando o usuário clica em um item, uma TextView muda para o conteúdo do item clicado.

A correspondência de ícones visíveis nas duas barras de ações é simples, conforme mostrado neste snippet de código:

Kotlin

    fun testClickActionBarItem() {
        // We make sure the contextual action bar is hidden.
        onView(withId(R.id.hide_contextual_action_bar))
            .perform(click())

        // Click on the icon - we can find it by the r.Id.
        onView(withId(R.id.action_save))
            .perform(click())

        // Verify that we have really clicked on the icon
        // by checking the TextView content.
        onView(withId(R.id.text_action_bar_result))
            .check(matches(withText("Save")))
    }
    

Java

    public void testClickActionBarItem() {
        // We make sure the contextual action bar is hidden.
        onView(withId(R.id.hide_contextual_action_bar))
            .perform(click());

        // Click on the icon - we can find it by the r.Id.
        onView(withId(R.id.action_save))
            .perform(click());

        // Verify that we have really clicked on the icon
        // by checking the TextView content.
        onView(withId(R.id.text_action_bar_result))
            .check(matches(withText("Save")));
    }
    

O botão

O código é o mesmo para a barra de ações contextual:

Kotlin

    fun testClickActionModeItem() {
        // Make sure we show the contextual action bar.
        onView(withId(R.id.show_contextual_action_bar))
            .perform(click())

        // Click on the icon.
        onView((withId(R.id.action_lock)))
            .perform(click())

        // Verify that we have really clicked on the icon
        // by checking the TextView content.
        onView(withId(R.id.text_action_bar_result))
            .check(matches(withText("Lock")))
    }
    

Java

    public void testClickActionModeItem() {
        // Make sure we show the contextual action bar.
        onView(withId(R.id.show_contextual_action_bar))
            .perform(click());

        // Click on the icon.
        onView((withId(R.id.action_lock)))
            .perform(click());

        // Verify that we have really clicked on the icon
        // by checking the TextView content.
        onView(withId(R.id.text_action_bar_result))
            .check(matches(withText("Lock")));
    }
    

O botão

Clicar nos itens no menu flutuante é um pouco mais complicado para a barra de ações normal, porque alguns dispositivos têm um botão físico de menu flutuante, que abre os itens flutuantes em um menu de opções, enquanto outros têm um botão virtual de menu flutuante, que abre um menu flutuante normal. Felizmente, o Espresso resolve isso.

Para a barra de ações normal:

Kotlin

    fun testActionBarOverflow() {
        // Make sure we hide the contextual action bar.
        onView(withId(R.id.hide_contextual_action_bar))
            .perform(click())

        // Open the options menu OR open the overflow menu, depending on whether
        // the device has a hardware or software overflow menu button.
        openActionBarOverflowOrOptionsMenu(
                ApplicationProvider.getApplicationContext<Context>())

        // Click the item.
        onView(withText("World"))
            .perform(click())

        // Verify that we have really clicked on the icon by checking
        // the TextView content.
        onView(withId(R.id.text_action_bar_result))
            .check(matches(withText("World")))
    }
    

Java

    public void testActionBarOverflow() {
        // Make sure we hide the contextual action bar.
        onView(withId(R.id.hide_contextual_action_bar))
            .perform(click());

        // Open the options menu OR open the overflow menu, depending on whether
        // the device has a hardware or software overflow menu button.
        openActionBarOverflowOrOptionsMenu(
                ApplicationProvider.getApplicationContext());

        // Click the item.
        onView(withText("World"))
            .perform(click());

        // Verify that we have really clicked on the icon by checking
        // the TextView content.
        onView(withId(R.id.text_action_bar_result))
            .check(matches(withText("World")));
    }
    

O botão do menu flutuante fica visível, e uma lista aparece abaixo da barra de ações, na parte superior da tela

Esta é a aparência da barra em dispositivos com um botão físico de menu flutuante:

Não há um botão de menu flutuante, e uma lista aparece na parte inferior da tela

Para a barra de ações contextual, o código também é fácil:

Kotlin

    fun testActionModeOverflow() {
        // Show the contextual action bar.
        onView(withId(R.id.show_contextual_action_bar))
            .perform(click())

        // Open the overflow menu from contextual action mode.
        openContextualActionModeOverflowMenu()

        // Click on the item.
        onView(withText("Key"))
            .perform(click())

        // Verify that we have really clicked on the icon by
        // checking the TextView content.
        onView(withId(R.id.text_action_bar_result))
            .check(matches(withText("Key")))
        }
    }
    

Java

    public void testActionModeOverflow() {
        // Show the contextual action bar.
        onView(withId(R.id.show_contextual_action_bar))
            .perform(click());

        // Open the overflow menu from contextual action mode.
        openContextualActionModeOverflowMenu();

        // Click on the item.
        onView(withText("Key"))
            .perform(click());

        // Verify that we have really clicked on the icon by
        // checking the TextView content.
        onView(withId(R.id.text_action_bar_result))
            .check(matches(withText("Key")));
        }
    }
    

O botão de menu flutuante aparece na barra de ações, e a lista de opções aparece abaixo dela, perto da parte superior da tela

Para ver o código completo dessas amostras, consulte o exemplo ActionBarTest.java no GitHub (em inglês).

Declarar que uma visualização não é exibida

Depois de realizar uma série de ações, é recomendável declarar o estado da IU em teste. Às vezes, esse pode ser um caso negativo, por exemplo, quando algo não está acontecendo. Lembre-se de que é possível transformar qualquer matcher de visualização de Hamcrest em um ViewAssertion usando ViewAssertions.matches().

No exemplo abaixo, usamos o matcher isDisplayed() e o revertemos usando o matcher not() padrão:

Kotlin

    import androidx.test.espresso.Espresso.onView
    import androidx.test.espresso.assertion.ViewAssertions.matches
    import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
    import androidx.test.espresso.matcher.ViewMatchers.withId
    import org.hamcrest.Matchers.not

    onView(withId(R.id.bottom_left))
        .check(matches(not(isDisplayed())))
    

Java

    import static androidx.test.espresso.Espresso.onView;
    import static androidx.test.espresso.assertion.ViewAssertions.matches;
    import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
    import static androidx.test.espresso.matcher.ViewMatchers.withId;
    import static org.hamcrest.Matchers.not;

    onView(withId(R.id.bottom_left))
        .check(matches(not(isDisplayed())));
    

A abordagem acima funcionará se a visualização ainda fizer parte da hierarquia. Caso contrário, você receberá um NoMatchingViewException e precisará usar ViewAssertions.doesNotExist().

Declarar que uma visualização não está presente

Se a visualização sair da hierarquia de visualização, o que pode acontecer quando uma ação causa uma transição para outra atividade, use ViewAssertions.doesNotExist():

Kotlin

    import androidx.test.espresso.Espresso.onView
    import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
    import androidx.test.espresso.matcher.ViewMatchers.withId

    onView(withId(R.id.bottom_left))
        .check(doesNotExist())
    

Java

    import static androidx.test.espresso.Espresso.onView;
    import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist;
    import static androidx.test.espresso.matcher.ViewMatchers.withId;

    onView(withId(R.id.bottom_left))
        .check(doesNotExist());
    

Declarar que um item de dados não está em um adaptador

Para comprovar que um item de dados específico não está em um AdapterView, é necessário adotar outra abordagem. É preciso localizar o AdapterView do seu interesse e interrogar os dados que ela retém. Não é necessário usar onData(). Em vez disso, use onView() para encontrar AdapterView e, depois, use outro matcher para trabalhar nos dados dentro da visualização.

Comece pelo matcher:

Kotlin

    private fun withAdaptedData(dataMatcher: Matcher<Any>): Matcher<View> {
        return object : TypeSafeMatcher<View>() {

            override fun describeTo(description: Description) {
                description.appendText("with class name: ")
                dataMatcher.describeTo(description)
            }

            public override fun matchesSafely(view: View) : Boolean {
                if (view !is AdapterView<*>) {
                    return false
                }

                val adapter = view.adapter
                for (i in 0 until adapter.count) {
                    if (dataMatcher.matches(adapter.getItem(i))) {
                        return true
                    }
                }

                return false
            }
        }
    }
    

Java

    private static Matcher<View> withAdaptedData(final Matcher<Object> dataMatcher) {
        return new TypeSafeMatcher<View>() {

            @Override
            public void describeTo(Description description) {
                description.appendText("with class name: ");
                dataMatcher.describeTo(description);
            }

            @Override
            public boolean matchesSafely(View view) {
                if (!(view instanceof AdapterView)) {
                    return false;
                }

                @SuppressWarnings("rawtypes")
                Adapter adapter = ((AdapterView) view).getAdapter();
                for (int i = 0; i < adapter.getCount(); i++) {
                    if (dataMatcher.matches(adapter.getItem(i))) {
                        return true;
                    }
                }

                return false;
            }
        };
    }
    

Em seguida, basta usar onView() para encontrar a AdapterView:

Kotlin

    fun testDataItemNotInAdapter() {
        onView(withId(R.id.list))
              .check(matches(not(withAdaptedData(withItemContent("item: 168")))))
        }
    }
    

Java

    @SuppressWarnings("unchecked")
    public void testDataItemNotInAdapter() {
        onView(withId(R.id.list))
              .check(matches(not(withAdaptedData(withItemContent("item: 168")))));
        }
    }
    

Além disso, temos uma declaração que falhará se um item igual a "item: 168" existir em uma visualização do adaptador com a lista de códigos.

Para ver o exemplo completo, observe o método testDataItemNotInAdapter() na classe AdapterViewTest.java no GitHub.

Usar um gerenciador de falhas personalizado

A substituição do FailureHandler padrão no Espresso por um personalizado permite o processamento de erros extras ou diferentes, por exemplo, fazer uma captura de tela ou transmitir informações de depuração extras.

O exemplo CustomFailureHandlerTest mostra como implementar um gerenciador de falhas personalizado:

Kotlin

    private class CustomFailureHandler(targetContext: Context) : FailureHandler {
        private val delegate: FailureHandler

        init {
            delegate = DefaultFailureHandler(targetContext)
        }

        override fun handle(error: Throwable, viewMatcher: Matcher<View>) {
            try {
                delegate.handle(error, viewMatcher)
            } catch (e: NoMatchingViewException) {
                throw MySpecialException(e)
            }

        }
    }
    

Java

    private static class CustomFailureHandler implements FailureHandler {
        private final FailureHandler delegate;

        public CustomFailureHandler(Context targetContext) {
            delegate = new DefaultFailureHandler(targetContext);
        }

        @Override
        public void handle(Throwable error, Matcher<View> viewMatcher) {
            try {
                delegate.handle(error, viewMatcher);
            } catch (NoMatchingViewException e) {
                throw new MySpecialException(e);
            }
        }
    }
    

Esse gerenciador de falhas gera uma MySpecialException em vez de um NoMatchingViewException e delega todas as outras falhas ao DefaultFailureHandler. O CustomFailureHandler pode ser registrado no Espresso no método setUp() do teste:

Kotlin

    @Throws(Exception::class)
    override fun setUp() {
        super.setUp()
        getActivity()
        setFailureHandler(CustomFailureHandler(
                ApplicationProvider.getApplicationContext<Context>()))
    }
    

Java

    @Override
    public void setUp() throws Exception {
        super.setUp();
        getActivity();
        setFailureHandler(new CustomFailureHandler(
                ApplicationProvider.getApplicationContext()));
    }
    

Para ver mais informações, consulte as interfaces FailureHandler e Espresso.setFailureHandler().

Segmentar janelas não padrão

O Android é compatível com várias janelas. Normalmente, isso fica claro para os usuários e para o desenvolvedor de apps. Mas, em certos casos, várias janelas ficam visíveis, por exemplo, quando uma janela de preenchimento automático é desenhada sobre a janela principal do app no widget de pesquisa. Por padrão, o Espresso usa uma heurística para identificar com qual Window você pretende interagir e, assim, simplifica o processo. Essa heurística é quase sempre suficiente. No entanto, em casos raros, é necessário especificar a janela que a interação segmentará. Para fazer isso, informe seu matcher de janela raiz ou o matcher Root:

Kotlin

    onView(withText("South China Sea"))
        .inRoot(withDecorView(not(`is`(getActivity().getWindow().getDecorView()))))
        .perform(click())
    

Java

    onView(withText("South China Sea"))
        .inRoot(withDecorView(not(is(getActivity().getWindow().getDecorView()))))
        .perform(click());
    

Como acontece com ViewMatchers, disponibilizamos um conjunto de RootMatchers predefinidos. Obviamente, você sempre pode implementar seu objeto Matcher.

Consulte a amostra MultipleWindowTest (em inglês) no GitHub.

Cabeçalhos e rodapés são adicionados a ListViews por meio dos métodos addHeaderView() e addFooterView(). Para garantir que Espresso.onData() saiba qual objeto de dados associar, transmita um valor de objeto de dados predefinido como o segundo parâmetro para addHeaderView() e addFooterView(). Exemplo:

Kotlin

    const val FOOTER = "FOOTER"
    ...
    val footerView = layoutInflater.inflate(R.layout.list_item, listView, false)
    footerView.findViewById<TextView>(R.id.item_content).text = "count:"
    footerView.findViewById<TextView>(R.id.item_size).text
            = data.size.toString
    listView.addFooterView(footerView, FOOTER, true)
    

Java

    public static final String FOOTER = "FOOTER";
    ...
    View footerView = layoutInflater.inflate(R.layout.list_item, listView, false);
    footerView.findViewById<TextView>(R.id.item_content).setText("count:");
    footerView.findViewById<TextView>(R.id.item_size).setText(String.valueOf(data.size()));
    listView.addFooterView(footerView, FOOTER, true);
    

Em seguida, programe um matcher para o rodapé:

Kotlin

    import org.hamcrest.Matchers.allOf
    import org.hamcrest.Matchers.instanceOf
    import org.hamcrest.Matchers.`is`

    fun isFooter(): Matcher<Any> {
        return allOf(`is`(instanceOf(String::class.java)),
                `is`(LongListActivity.FOOTER))
    }
    

Java

    import static org.hamcrest.Matchers.allOf;
    import static org.hamcrest.Matchers.instanceOf;
    import static org.hamcrest.Matchers.is;

    @SuppressWarnings("unchecked")
    public static Matcher<Object> isFooter() {
        return allOf(is(instanceOf(String.class)), is(LongListActivity.FOOTER));
    }
    

O carregamento da visualização em um teste é simples:

Kotlin

    import androidx.test.espresso.Espresso.onData
    import androidx.test.espresso.action.ViewActions.click
    import androidx.test.espresso.sample.LongListMatchers.isFooter

    fun testClickFooter() {
        onData(isFooter())
            .perform(click())

        // ...
    }
    

Java

    import static androidx.test.espresso.Espresso.onData;
    import static androidx.test.espresso.action.ViewActions.click;
    import static androidx.test.espresso.sample.LongListMatchers.isFooter;

    public void testClickFooter() {
        onData(isFooter())
            .perform(click());

        // ...
    }
    

Dê uma olhada no exemplo de código completo, encontrado no testClickFooter() método de AdapterViewTest.java no GitHub (em inglês).