Google se compromete a impulsar la igualdad racial para las comunidades afrodescendientes. Obtén información al respecto.

Recetas de Espresso

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:

Actividad de lista que muestra tres copias del mismo elemento de vista dentro de una lista de tres elementos

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 botón de guardar está en la barra de acciones, en la parte superior de la actividad.

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")));
    }
    

El botón de bloqueo está en la barra de acciones, en la parte superior de la actividad.

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")));
    }
    

El botón del menú ampliado es visible y se muestra una lista debajo de la barra de acciones, cerca de la parte superior de la pantalla.

Esta es la apariencia en dispositivos que incluyen un botón de menú ampliado de hardware:

No hay un botón de menú ampliado y se muestra una lista, cerca de la parte inferior de la pantalla.

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")));
        }
    }
    

El botón de menú ampliado se muestra en la barra de acciones, y la lista de opciones aparece debajo de la barra de acciones, cerca de la parte superior de la pantalla.

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.

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.