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ó. Para exemplo, um botão de chamada repetida em uma tabela de contatos pode ter o mesmo R.id, contêm o mesmo texto e têm as mesmas propriedades de outras chamadas na hierarquia de visualização.

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

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

Muitas vezes, a visualização não exclusiva será pareada com um marcador exclusivo localizado ao lado dele, como o nome do contato ao lado do botão de chamada. Nesse caso, é possível usar o matcher 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 barra de ação e uma barra de ação contextual que é criada a partir de um menu de opções. Ambos as barras de ações têm um item que fica sempre visível e dois itens que estão apenas visível no menu flutuante. Quando um item recebe um clique, uma TextView muda para a conteúdo do item clicado.

A correspondência de ícones visíveis em ambas as barras de ações é direta, como mostrado no seguinte 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 "Salvar" fica na barra de ações, na parte superior da atividade

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 "Bloquear" fica na barra de ações, na parte superior da atividade

Clicar em itens no menu flutuante é um pouco mais complicado para a ação normal pois alguns dispositivos têm um botão físico de menu flutuante, que abre a itens excedentes em um menu de opções, e alguns dispositivos têm um menu flutuante de software. , que abre um menu flutuante normal. Felizmente, o Espresso lida com isso para nós.

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 do
          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 de baixo da tela
          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 do menu flutuante aparece na barra de ações, e a lista de
          aparecem abaixo da barra de ações, perto do topo da tela

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

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

Depois de executar uma série de ações, você certamente desejará declarar o estado da interface em teste. Às vezes, esse pode ser um caso negativo, como quando que algo não está acontecendo. Lembre-se de que você pode transformar qualquer visualização de Hamcrest correspondente 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. Se for não, você recebe uma NoMatchingViewException e precisa usar ViewAssertions.doesNotExist().

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

Se a visualização sair da hierarquia, o que pode acontecer quando uma causou uma transição para outra atividade. Use o 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 provar que um item de dados específico não está em uma AdapterView, você precisa fazer as coisas de modo um pouco diferente. Precisamos encontrar os AdapterView relevantes. e interroga os dados que ela contém. Não é necessário usar onData(). Em vez disso, usamos onView() para encontrar o AdapterView e depois outro 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")))));
    }
}

Temos também uma declaração que falhará se um item for igual a "item: 168". existe em uma visualização de adaptador com a lista de IDs.

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

Usar um gerenciador de falhas personalizado

Substituir o FailureHandler padrão no Espresso por um personalizado permite a tratamento de erros adicional ou diferente, como fazer uma captura de tela ou transmitir além de informações extras de depuração.

O exemplo CustomFailureHandlerTest demonstra como implementar uma gerenciador de falhas:

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 uma NoMatchingViewException e delega todas as outras falhas ao DefaultFailureHandler O CustomFailureHandler pode ser registrado 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 mais informações, consulte a FailureHandler interface e Espresso.setFailureHandler().

Segmentar janelas não padrão

O Android é compatível com várias janelas. Normalmente, isso é transparente para os usuários e o desenvolvedor do aplicativo, mas, em certos casos, várias janelas ficam visíveis, como como quando uma janela de preenchimento automático é desenhada sobre a janela principal do aplicativo no widget de pesquisa. Para simplificar as coisas, por padrão, o Espresso usa uma heurística para adivinhar com qual Window você pretende interagir. Essa heurística é quase é sempre bom o suficiente; No entanto, em casos raros, é necessário especificar a janela que uma interação deve segmentar. É possível fazer isso fornecendo sua própria janela raiz correspondente ou 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());

Assim como acontece com ViewMatchers, oferecemos um conjunto de RootMatchers Obviamente, você sempre pode implementar seu objeto Matcher.

Confira a API MultipleWindowTest amostra no GitHub.

Os cabeçalhos e rodapés são adicionados a ListViews usando addHeaderView() e addFooterView(). Para garantir que Espresso.onData() saiba qual objeto de dados 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());

    // ...
}

Veja o exemplo de código completo, encontrado no método testClickFooter() de AdapterViewTest.java no GitHub.