En este documento se describe cómo configurar una serie de pruebas de Espresso comunes.
Cómo hacer coincidir una vista junto a otra
Un diseño puede contener ciertas vistas que no son únicas por sí solas. Por ejemplo, un botón de llamada recurrente de una tabla de contactos podría tener el mismo R.id
, el mismo texto y las mismas propiedades que otros botones de llamada dentro de la jerarquía de vistas.
En esta actividad de ejemplo, la vista con el texto "7"
se repite en varias filas:
A menudo, se vincula la vista que no es única con alguna etiqueta única que se encuentra junto a ella, como el nombre del contacto junto al botón de llamada. En ese caso, puedes usar el comparador hasSibling()
para acotar la selección:
Kotlin
onView(allOf(withText("7"), hasSibling(withText("item: 0")))) .perform(click())
Java
onView(allOf(withText("7"), hasSibling(withText("item: 0")))) .perform(click());
Cómo hacer coincidir una vista que está dentro de una barra de acciones
La ActionBarTestActivity
tiene dos barras de acciones diferentes: una normal y otra de acciones contextuales, que se crea a partir de un menú de opciones. Ambas barras de acciones tienen un elemento que siempre está visible y dos elementos que solo están visibles en el menú ampliado. Cuando se hace clic en un elemento, se reemplaza el TextView por el contenido del elemento seleccionado.
Hacer coincidir los íconos visibles en ambas barras de acción es sencillo, como se muestra en el siguiente fragmento 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"))); }
El código es idéntico al de la barra de acciones contextuales:
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"))); }
Hacer clic en los elementos del menú ampliado es un poco más complicado en el caso de la barra de acciones normal, ya que algunos dispositivos tienen un botón de menú ampliado de hardware, que abre los elementos adicionales en un menú de opciones, y otros tienen un botón de menú ampliado de software, que abre un menú ampliado normal. Afortunadamente, Espresso se encarga de esa tarea por nosotros.
Barra de acciones 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 es la apariencia en dispositivos que incluyen un botón de menú ampliado de hardware:
En el caso de la barra de acciones contextuales, el procedimiento es muy sencillo también:
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 el código completo de estas muestras, consulta la muestra de ActionBarTest.java
en GitHub.
Cómo confirmar que no se muestra una vista
Después de realizar una serie de acciones, sin duda querrás confirmar el estado de la IU que se está probando. A veces el resultado puede ser negativo, como cuando algo no está sucediendo. Ten en cuenta que puedes convertir cualquier comparador de vistas de Hamcrest en una ViewAssertion
mediante ViewAssertions.matches()
.
En el siguiente ejemplo, se toma el comparador isDisplayed()
y se revierte mediante el comparador estándar not()
:
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())));
El enfoque anterior funciona si la vista sigue siendo parte de la jerarquía. Si no es así, se generará una NoMatchingViewException
y deberás usar ViewAssertions.doesNotExist()
.
Cómo confirmar que una vista no está presente
Si la vista desapareció de la jerarquía de vistas (lo que puede suceder cuando una acción causó una transición a otra actividad), deberás usar 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());
Cómo confirmar que un elemento de datos no está en un adaptador
Para comprobar que un elemento de datos específico no está dentro de una AdapterView
, debes proceder de manera algo diferente. Es necesario encontrar la AdapterView
deseada e interrogar los datos que contiene. No hace falta usar onData()
.
En cambio, se usa onView()
para encontrar la AdapterView
y luego, se utiliza otro comparador para trabajar en los datos dentro de la vista.
Primero el comparador:
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; } }; }
Luego, todo lo que se necesita es onView()
para encontrar la 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"))))); } }
Además, hay una aserción que fallará si existe un elemento igual a "item: 168" en una vista de adaptador con la lista de ID.
Para ver la muestra completa, consulta en GitHub el método testDataItemNotInAdapter()
dentro de la clase AdapterViewTest.java
.
Cómo usar un controlador de fallas personalizado
Reemplazar el FailureHandler
predeterminado en Espresso por uno personalizado permite implementar controles de errores adicionales o diferentes, como tomar una captura de pantalla o pasar información de depuración adicional.
El ejemplo de CustomFailureHandlerTest
muestra cómo implementar un controlador de fallas 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); } } }
Este controlador de fallas arroja una MySpecialException
en lugar de una NoMatchingViewException
, y delega todas las demás fallas al DefaultFailureHandler
. Se puede registrar el CustomFailureHandler
con Espresso en el método setUp()
de la prueba:
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 obtener más información, consulta la interfaz de FailureHandler
y Espresso.setFailureHandler()
.
Cómo orientar interacciones a ventanas no predeterminadas
Android es compatible con el uso de varias ventanas. En general, esta funcionalidad no plantea problemas para los usuarios ni para el desarrollador de la app; sin embargo, en ciertos casos hay varias ventanas visibles, como cuando se superponen una ventana de autocompletar y la ventana principal de la aplicación en el Widget de la Búsqueda. Para simplificar la situación, de forma predeterminada Espresso utiliza una heurística para adivinar con qué Window
deseas interactuar. Esta heurística es casi siempre correcta, pero en casos excepcionales deberás especificar a qué ventana orientar una interacción. Para ello, proporciona tu propio comparador de ventanas raíz o de 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 en el caso de los ViewMatchers
, se incluye un conjunto de RootMatchers
proporcionados previamente.
Por supuesto, siempre puedes implementar tu propio objeto Matcher
.
Consulta la muestra de MultipleWindowTest en GitHub.
Cómo hacer coincidir un encabezado o un pie de página en una vista de lista
Los encabezados y pies de página se agregan a ListViews
empleando los métodos addHeaderView()
y addFooterView()
. Para garantizar que Espresso.onData()
sepa con qué objeto de datos establecer la coincidencia, asegúrate de pasar un valor de objeto de datos predeterminado como segundo parámetro a addHeaderView()
y addFooterView()
. Por ejemplo:
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);
Luego puedes crear un comparador para el pie de página:
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)); }
Y cargar la vista en una prueba es muy sencillo:
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()); // ... }
Consulta la muestra de código completa que se encuentra en el método testClickFooter()
de AdapterViewTest.java
en GitHub.