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:
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 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"))); }
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"))); }
Esta é a aparência da barra em dispositivos com um botão físico de menu flutuante:
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"))); } }
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.
Associar um cabeçalho ou rodapé em uma visualização de lista
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).