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 que 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:
Muitas vezes, a visualização não exclusiva é pareada com um rótulo exclusivo localizado
ao lado dela, como o nome do contato ao lado do botão de chamada. Nesse caso,
use o matcher hasSibling()
para restringir a 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. As duas
barras de ações têm um item que fica sempre visível e dois itens que ficam apenas
visíveis no menu flutuante. Quando um item é clicado, ele muda uma TextView 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 em itens no menu flutuante é um pouco mais complicado para a barra de ações normal, já que alguns dispositivos têm um botão físico de menu flutuante, que abre os itens flutuantes em um menu de opções, e outros têm um botão de menu flutuante de software, que abre um menu flutuante normal. Felizmente, o Espresso cuida disso 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"))); }
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
(link em inglês) no GitHub.
Declarar que uma visualização não é exibida
Depois de realizar uma série de ações, declare o
estado da interface em teste. Às vezes, esse pode ser um caso negativo, como quando
algo não está acontecendo. Lembre-se de que é possível transformar qualquer matcher de visualização
do 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á uma NoMatchingViewException
e precisará 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
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 uma AdapterView
, é necessário adotar
medidas um pouco diferentes. Precisamos encontrar o AdapterView
em que estamos interessados
e interrogar os dados que ela retém. Não é necessário usar onData()
.
Em vez disso, vamos usar onView()
para encontrar o AdapterView
e, em seguida, usar 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"))))); } }
Temos uma declaração que falhará se um item igual a "item: 168" existir em uma visualização de adaptador com a lista de IDs.
Para ver o exemplo completo, observe o método testDataItemNotInAdapter()
na classe
AdapterViewTest.java
(link em inglês)
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 adicionais ou diferentes, como 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 uma
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 mais informações, consulte a interface
FailureHandler
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 para o desenvolvedor de apps. No entanto, em alguns casos, várias janelas ficam visíveis, como
quando uma janela de preenchimento automático é desenhada sobre a janela principal do aplicativo no
widget de pesquisa. Por padrão, o Espresso usa uma heurística para
adivinhar com qual Window
você pretende interagir, o que simplifica o processo. Essa heurística é quase
sempre boa o suficiente. No entanto, em casos raros, é necessário especificar a janela
que a interação precisa segmentar. Para fazer isso, informe o próprio 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
,
fornecemos um conjunto de
RootMatchers
pré-fornecidos.
Obviamente, você sempre pode implementar seu objeto Matcher
.
Confira o exemplo 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
usando os métodos addHeaderView()
e
addFooterView()
. Para garantir que Espresso.onData()
saiba qual objeto de dados
corresponder, transmita um valor predefinido de objeto de dados como o segundo parâmetro
para addHeaderView()
e addFooterView()
. Por 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()); // ... }
Confira o exemplo de código completo, encontrado no método testClickFooter()
de
AdapterViewTest.java
(link em inglês)
no GitHub.