Recetas de Espresso

En este documento, se describe cómo configurar una variedad de pruebas Espresso comunes.

Cómo hacer coincidir una vista junto a otra vista

Un diseño puede contener ciertas vistas que no son únicas por sí mismas. Por ejemplo, un botón de llamada repetida en una tabla de contactos puede tener el mismo R.id, contener el mismo texto y tener las mismas propiedades que otros botones de llamada dentro de la jerarquía de vista.

Por ejemplo, en esta actividad, la vista con el texto "7" repite en varias filas:

Una actividad de lista que muestra tres copias del mismo elemento de vista dentro de una lista de 3 elementos.

A menudo, la vista que no es única se sincroniza con alguna etiqueta única que se encuentra junto a ella, como el nombre del contacto junto al botón de llamada. En este caso, puedes usar hasSibling() para acotar tu 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 acción

ActionBarTestActivity tiene dos barras de acción diferentes: una normal y otra contextual que se crea a partir de un menú de opciones. Ambas barras de acción 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, cambia TextView al 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 para guardar está en la barra de acción, en la parte superior de la actividad.

El código es idéntico al de la barra de acción 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")));
    }
    

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 para la barra de acción normal, ya que algunos dispositivos tienen un botón de menú ampliado de hardware, que abre los elementos ampliados en un menú de opciones, y algunos dispositivos tienen un botón de menú ampliado de software, que abre un botón normal de menú ampliado. Afortunadamente, Espresso se encarga de eso por nosotros.

Para la barra de acción 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 acción cerca de la parte superior de la pantalla.

Esta es la apariencia en dispositivos con 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.

Para la barra de acción contextual, es también muy sencillo:

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 acción, y la lista de opciones se muestra debajo de la barra de acción, cerca de la parte superior de la pantalla.

Para ver el código completo de estos ejemplos, consulta el ejemplo de ActionBarTest.java en GitHub.

Cómo afirmar que no se muestra una vista

Después de realizar una serie de acciones, seguramente querrás afirmar el estado de la IU que estás probando. En ocasiones, este puede ser un caso negativo, por ejemplo, comprobar que algo no se está realizando. Ten en cuenta que puedes convertir cualquier comparador de vistas de hamcrest en ViewAssertion utilizando ViewAssertions.matches().

En el siguiente ejemplo, tomamos el comparador isDisplayed() y lo invertimos usando 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. De lo contrario, obtendrás NoMatchingViewException y deberás usar ViewAssertions.doesNotExist().

Cómo afirmar que una vista no está presente

Si la vista desapareció de la jerarquía de vistas, lo que puede suceder si una acción provocó una transición a otra actividad, debes 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 afirmar que un elemento de datos no está en un adaptador

Para probar que un elemento de datos determinado no está dentro de AdapterView, debes cambiar el curso de acción. Es necesario encontrar el AdapterView deseado e interrogar los datos que contiene. No es necesario usar onData(). En cambio, se usa onView() para encontrar AdapterView y luego se usa 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;
            }
        };
    }
    

Todo lo que se necesita es que onView() encuentre 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 afirmación que fallará si existe un elemento que es igual a "item: 168" en una vista de adaptador con la lista de ID.

Para ver el ejemplo completo, consulte el método testDataItemNotInAdapter() dentro de la clase AdapterViewTest.java en GitHub.

Cómo usar un controlador de fallas personalizado

Si se reemplaza el FailureHandler predeterminado de Espresso por uno personalizado, se puede controlar errores adicionales o diferentes, como tomar una captura de pantalla o pasar información de depuración adicional.

En el ejemplo de CustomFailureHandlerTest, se 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 genera una instancia de MySpecialException en lugar de NoMatchingViewException, y delega todas las demás fallas en DefaultFailureHandler. CustomFailureHandler se puede registrar 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 dirigir ventanas no predeterminadas

Android es compatible con varias ventanas. En general, son transparentes para los usuarios y el desarrollador de la app; sin embargo, en ciertos casos, son visibles varias ventanas, como cuando se genera una ventana de autocompletar sobre la ventana principal de la aplicación en el widget de búsqueda. Para simplificar esto, de forma predeterminada, Espresso utiliza una heurística para adivinar con qué Window quieres interactuar. Esta heurística es casi siempre lo suficientemente buena; sin embargo, en casos excepcionales, deberás especificar a qué ventana deberá dirigirse una interacción. Para ello, proporciona tu propio comparador de ventanas raíz, o comparador 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 es el caso con ViewMatchers, proporcionamos previamente un conjunto de RootMatchers. Por supuesto, siempre puedes implementar tu propio objeto Matcher.

Consulta el ejemplo de MultipleWindowTest en GitHub.

Los encabezados y los pies de página se agregan a ListViews utilizando los métodos addHeaderView() y addFooterView(). Para asegurarte de que Espresso.onData() sepa qué objeto de datos debe coincidir, asegúrate de pasar un valor de objeto de datos predeterminado como el 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, puede escribir 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));
    }
    

Además, 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 el ejemplo de código completo, que se encuentra en el método testClickFooter() de AdapterViewTest.java en GitHub.