Conceptos básicos de Espresso

En este documento, se explica cómo completar tareas de prueba automatizadas comunes con la API de Espresso.

La API de Espresso alienta a los autores de pruebas a pensar en términos de lo que un usuario podría hacer mientras interactúa con la aplicación: ubicar elementos de la IU e interactuar con ellos. Al mismo tiempo, el framework impide el acceso directo a las actividades y las vistas de la aplicación porque conservar estos objetos y realizar operaciones sobre ellos fuera del subproceso de IU es una fuente importante de inestabilidad en las pruebas. Por lo tanto, no verás métodos como getView() y getCurrentActivity() en la API de Espresso. Aún puedes realizar operaciones en las vistas de forma segura mediante la implementación de tus propias subclases de ViewAction y ViewAssertion.

Componentes de la API

Estos son algunos de los componentes principales de Espresso:

  • Espresso: Punto de entrada a las interacciones con las vistas (a través de onView() y onData()). También expone las APIs que no están necesariamente vinculadas a ninguna vista, como pressBack().
  • ViewMatchers: Es una colección de objetos que implementan la interfaz Matcher<? super View>. Puedes pasar uno o más de estos al método onView() para ubicar una vista dentro de la jerarquía de vistas actual.
  • ViewActions: Es una colección de objetos ViewAction que se pueden pasar al método ViewInteraction.perform(), como click().
  • ViewAssertions: Es una colección de objetos ViewAssertion a los que se puede pasar el método ViewInteraction.check(). La mayoría de las veces, usarás la aserción de coincidencias, que utiliza un comparador de vistas para confirmar el estado de la vista seleccionada.

Ejemplo:

Kotlin

// withId(R.id.my_view) is a ViewMatcher
// click() is a ViewAction
// matches(isDisplayed()) is a ViewAssertion
onView(withId(R.id.my_view))
    .perform(click())
    .check(matches(isDisplayed()))

Java

// withId(R.id.my_view) is a ViewMatcher
// click() is a ViewAction
// matches(isDisplayed()) is a ViewAssertion
onView(withId(R.id.my_view))
    .perform(click())
    .check(matches(isDisplayed()));

Cómo encontrar una vista

En la gran mayoría de los casos, el método onView() toma un comparador de Hamcrest que se espera que coincida con una sola vista dentro de la jerarquía de vistas actual. Los comparadores son potentes y serán familiares para quienes los hayan usado con Mockito o JUnit. Si no conoces los comparadores de Hamcrest, te sugerimos que comiences con una vista rápida de esta presentación.

A menudo, la vista deseada tiene un R.id único, y un comparador withId simple reducirá la búsqueda de vistas. Sin embargo, hay muchos casos legítimos en los que no puedes determinar R.id al momento del desarrollo de la prueba. Por ejemplo, es posible que la vista específica no tenga un R.id o que el R.id no sea único. Esto puede hacer que las pruebas de instrumentación normales sean frágiles y difíciles de escribir, ya que la forma normal de acceder a la vista (con findViewById()) no funciona. Por lo tanto, es posible que debas acceder a miembros privados de la actividad o el fragmento que contiene la vista, o bien buscar un contenedor con un R.id conocido y navegar a su contenido para la vista en particular.

Espresso controla este problema correctamente, ya que te permite reducir la vista utilizando objetos ViewMatcher existentes o tus propios objetos personalizados.

Encontrar una vista por su R.id es tan simple como llamar a onView():

Kotlin

onView(withId(R.id.my_view))

Java

onView(withId(R.id.my_view));

A veces, los valores de R.id se comparten entre varias vistas. Cuando esto sucede, un intento de usar un R.id en particular genera una excepción, como AmbiguousViewMatcherException. El mensaje de excepción te proporciona una representación de texto de la jerarquía de vistas actual, en la que puedes buscar y encontrar las vistas que coincidan con el R.id no único:

java.lang.RuntimeException:
androidx.test.espresso.AmbiguousViewMatcherException
This matcher matches multiple views in the hierarchy: (withId: is <123456789>)

...

+----->SomeView{id=123456789, res-name=plus_one_standard_ann_button,
visibility=VISIBLE, width=523, height=48, has-focus=false, has-focusable=true,
window-focus=true, is-focused=false, is-focusable=false, enabled=true,
selected=false, is-layout-requested=false, text=,
root-is-layout-requested=false, x=0.0, y=625.0, child-count=1}
****MATCHES****
|
+------>OtherView{id=123456789, res-name=plus_one_standard_ann_button,
visibility=VISIBLE, width=523, height=48, has-focus=false, has-focusable=true,
window-focus=true, is-focused=false, is-focusable=true, enabled=true,
selected=false, is-layout-requested=false, text=Hello!,
root-is-layout-requested=false, x=0.0, y=0.0, child-count=1}
****MATCHES****

Si revisas los diversos atributos de las vistas, es posible que encuentres propiedades identificables de manera inequívoca. En el ejemplo anterior, una de las vistas tiene el texto "Hello!". Puedes usar esto para acotar la búsqueda usando comparadores de combinación:

Kotlin

onView(allOf(withId(R.id.my_view), withText("Hello!")))

Java

onView(allOf(withId(R.id.my_view), withText("Hello!")));

También puedes optar por no revertir ninguno de los comparadores:

Kotlin

onView(allOf(withId(R.id.my_view), not(withText("Unwanted"))))

Java

onView(allOf(withId(R.id.my_view), not(withText("Unwanted"))));

Consulta ViewMatchers para ver los comparadores que proporciona Espresso.

Consideraciones

  • En una app que se comporta correctamente, todas las vistas con las que un usuario puede interactuar deben contener texto descriptivo o tener una descripción del contenido. Consulta Cómo hacer que las apps sean más accesibles para obtener más detalles. Si no puedes acotar una búsqueda con withText() o withContentDescription(), procura tratarla como un error de accesibilidad.
  • Usa el comparador menos descriptivo que encuentre la vista que buscas. No especifiques más detalles, ya que esto forzará al framework a realizar más trabajo del necesario. Por ejemplo, si una vista se puede identificar de manera única por su texto, no necesitas especificar que la vista también se puede asignar desde TextView. Para muchas vistas, el R.id de la vista debería ser suficiente.
  • Si la vista de destino está dentro de una AdapterView, como ListView, GridView o Spinner, es posible que el método onView() no funcione. En esos casos, debes usar onData().

Cómo ejecutar una acción en una vista

Cuando hayas encontrado un comparador adecuado para la vista de destino, podrás realizar instancias de ViewAction en ella con el método de ejecución.

Por ejemplo, para hacer clic en la vista:

Kotlin

onView(...).perform(click())

Java

onView(...).perform(click());

Puedes ejecutar más de una acción con una llamada:

Kotlin

onView(...).perform(typeText("Hello"), click())

Java

onView(...).perform(typeText("Hello"), click());

Si la vista con la que trabajas está ubicada dentro de una ScrollView (vertical u horizontal), procura anteponer scrollTo() a las acciones que requieran que se muestre la vista, como click() y typeText(). Esto garantiza que la vista se mostrará antes de continuar con la otra acción:

Kotlin

onView(...).perform(scrollTo(), click())

Java

onView(...).perform(scrollTo(), click());

Consulta ViewActions para conocer las acciones de vista que proporciona Espresso.

Cómo verificar las aserciones de vistas

Las aserciones se pueden aplicar a la vista seleccionada con el método check(). La aserción más utilizada es matches(), Utiliza un objeto ViewMatcher para confirmar el estado de la vista seleccionada actualmente.

Por ejemplo, para verificar que una vista tenga el texto "Hello!", puede usarse el código siguiente:

Kotlin

onView(...).check(matches(withText("Hello!")))

Java

onView(...).check(matches(withText("Hello!")));

Si deseas confirmar que "Hello!" sea el contenido de la vista, no es recomendable que uses el código siguiente:

Kotlin

// Don't use assertions like withText inside onView.
onView(allOf(withId(...), withText("Hello!"))).check(matches(isDisplayed()))

Java

// Don't use assertions like withText inside onView.
onView(allOf(withId(...), withText("Hello!"))).check(matches(isDisplayed()));

Por otro lado, si deseas confirmar que una vista con el texto "Hello!" es visible (por ejemplo, después de un cambio en la marca de visibilidad de las vistas), el código es correcto.

Cómo visualizar una prueba de aserción simple

En este ejemplo, SimpleActivity contiene un Button y una TextView. Cuando se hace clic en el botón, el contenido de TextView cambia a "Hello Espresso!".

A continuación, se explica cómo probar el procedimiento anterior con Espresso:

Haz clic en el botón

El primer paso es buscar una propiedad que ayude a encontrar el botón. El botón de SimpleActivity tiene un R.id único, como se espera.

Kotlin

onView(withId(R.id.button_simple))

Java

onView(withId(R.id.button_simple));

Para hacer clic:

Kotlin

onView(withId(R.id.button_simple)).perform(click())

Java

onView(withId(R.id.button_simple)).perform(click());

Cómo verificar el texto de TextView

La TextView con el texto para verificar también tiene un R.id único:

Kotlin

onView(withId(R.id.text_simple))

Java

onView(withId(R.id.text_simple));

Para verificar el texto del contenido:

Kotlin

onView(withId(R.id.text_simple)).check(matches(withText("Hello Espresso!")))

Java

onView(withId(R.id.text_simple)).check(matches(withText("Hello Espresso!")));

Cómo verificar la carga de datos en las vistas de adaptador

AdapterView es un tipo especial de widget que carga sus datos de forma dinámica desde un adaptador. El ejemplo más común de una AdapterView es ListView. A diferencia de los widgets estáticos, como LinearLayout, solo se puede cargar un subconjunto de los elementos secundarios AdapterView en la jerarquía de vistas actual. Una búsqueda simple de onView() no encontraría las vistas que no están cargadas en ese momento.

Para controlar esto, Espresso proporciona un punto de entrada onData() independiente que puede cargar primero el elemento del adaptador en cuestión y ponerlo en foco antes de operar sobre él o cualquiera de sus elementos secundarios.

Advertencia: Las implementaciones personalizadas de AdapterView pueden tener problemas con el método onData() si no cumplen con los contratos de herencia, especialmente la API de getItem(). En esos casos, lo mejor es refactorizar el código de tu aplicación. Si no puedes hacerlo, puedes implementar una AdapterViewProtocol personalizada que coincida. Para obtener más información, consulta la clase AdapterViewProtocols predeterminada que proporciona Espresso.

Cómo implementar una prueba simple de la vista de adaptador

En esta prueba simple, se demuestra cómo usar onData(). SimpleActivity contiene un Spinner con algunos elementos que representan tipos de cafés. Cuando se selecciona un elemento, hay un TextView que cambia a "One %s a day!", donde %s representa el elemento seleccionado.

El objetivo de esta prueba es abrir el Spinner, seleccionar un elemento específico y verificar que el TextView contenga el elemento. Como la clase Spinner se basa en AdapterView, se recomienda usar onData() en lugar de onView() para hacer coincidir el elemento.

Cómo abrir la selección del elemento

Kotlin

onView(withId(R.id.spinner_simple)).perform(click())

Java

onView(withId(R.id.spinner_simple)).perform(click());

Cómo seleccionar un elemento

Para la selección del elemento, el Spinner crea una ListView con el contenido correspondiente. Esta vista puede ser muy larga, y es posible que el elemento no contribuya a la jerarquía de vistas. Si usamos onData(), forzamos el elemento deseado en la jerarquía de vistas. Los elementos de Spinner son strings, por lo que queremos hacer coincidir un elemento que sea igual a la string "Americano":

Kotlin

onData(allOf(`is`(instanceOf(String::class.java)),
        `is`("Americano"))).perform(click())

Java

onData(allOf(is(instanceOf(String.class)), is("Americano"))).perform(click());

Verifica que el texto sea correcto

Kotlin

onView(withId(R.id.spinnertext_simple))
    .check(matches(withText(containsString("Americano"))))

Java

onView(withId(R.id.spinnertext_simple))
    .check(matches(withText(containsString("Americano"))));

Depuración

Espresso proporciona información útil de depuración cuando falla una prueba:

Registros

Espresso registra todas las acciones de vistas en logcat. Por ejemplo:

ViewInteraction: Performing 'single click' action on view with text: Espresso

Jerarquía de vistas

Espresso imprime la jerarquía de vistas en el mensaje de excepción cuando falla onView().

  • Si onView() no encuentra la vista de destino, se arroja una NoMatchingViewException. Puedes examinar la jerarquía de vistas en la string de excepción para analizar por qué el comparador no coincide con ninguna vista.
  • Si onView() encuentra varias vistas que coinciden con el comparador dado, se genera una AmbiguousViewMatcherException. Se muestra la jerarquía de vistas y todas las vistas coincidentes se marcan con la etiqueta MATCHES:
java.lang.RuntimeException:
androidx.test.espresso.AmbiguousViewMatcherException
This matcher matches multiple views in the hierarchy: (withId: is <123456789>)

...

+----->SomeView{id=123456789, res-name=plus_one_standard_ann_button,
visibility=VISIBLE, width=523, height=48, has-focus=false, has-focusable=true,
window-focus=true, is-focused=false, is-focusable=false, enabled=true,
selected=false, is-layout-requested=false, text=,
root-is-layout-requested=false, x=0.0, y=625.0, child-count=1}
****MATCHES****
|
+------>OtherView{id=123456789, res-name=plus_one_standard_ann_button,
visibility=VISIBLE, width=523, height=48, has-focus=false, has-focusable=true,
window-focus=true, is-focused=false, is-focusable=true, enabled=true,
selected=false, is-layout-requested=false, text=Hello!,
root-is-layout-requested=false, x=0.0, y=0.0, child-count=1}
****MATCHES****

Cuando se trata de una jerarquía de vistas complicada o de un comportamiento inesperado de los widgets, siempre es útil usar Hierarchy Viewer en Android Studio para obtener una explicación.

Advertencias sobre la vista de adaptador

Espresso advierte a los usuarios sobre la presencia de widgets de AdapterView. Cuando una operación onView() genera una NoMatchingViewException y los widgets AdapterView están presentes en la jerarquía de vistas, la solución más común es usar onData(). El mensaje de excepción incluirá una advertencia con una lista de las vistas de adaptador. Puedes usar esta información a fin de invocar a onData() para cargar la vista de destino.

Recursos adicionales

Para obtener más información sobre el uso de Espresso en pruebas de Android, consulta los siguientes recursos.

Ejemplos