En este documento, se explica cómo completar tareas comunes de pruebas automatizadas 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 app: 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, ya que conservar esos objetos y realizar operaciones con ellos por fuera del subproceso de la IU suele causar gran inestabilidad en las pruebas. Por lo tanto, no verás métodos como getView()
y getCurrentActivity()
en la API de Espresso.
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()
yonData()
). También expone las API que no están necesariamente vinculadas a ninguna vista, comopressBack()
. - ViewMatchers: colección de objetos que implementan la interfaz
Matcher<? super View>
. Puedes pasar uno o más de estos al métodoonView()
para ubicar una vista dentro de la jerarquía de vistas actual. - ViewActions: colección de objetos
ViewAction
que se pueden pasar al métodoViewInteraction.perform()
, comoclick()
. - ViewAssertions: colección de objetos
ViewAssertion
a los que se puede pasar el métodoViewInteraction.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 eficaces y les resultarán familiares a quienes los hayan usado con Mockito o JUnit. Si no trabajaste antes con 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 limitará la búsqueda de vistas. Sin embargo, hay muchos casos legítimos en los que no se puede 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 inestables y complejas en cuanto a su escritura, porque la forma normal de acceder a la vista (con findViewById()
) no funciona. Por lo tanto, es posible que debas acceder a los miembros privados de la actividad o el fragmento que conservan la vista, o bien buscar un contenedor con un R.id
conocido y navegar a su contenido para llegar a la vista particular.
Espresso da una respuesta eficaz a este problema, ya que te permite acotar la vista empleando 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 eso sucede, si intentas usar un R.id
en particular, el sistema arroja una excepción, como AmbiguousViewMatcherException
. El mensaje de excepción te proporciona una representación de texto de la jerarquía de vistas actual, que puedes usar para buscar las vistas que coincidan con los R.id
no únicos:
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 observas los distintos atributos de las vistas, es posible que encuentres propiedades que se puedan identificar de forma única. En el ejemplo anterior, una de las vistas tiene el texto "Hello!"
. Puedes usar esto para acotar la búsqueda usando comparadores con combinaciones:
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 conocer los comparadores que proporciona Espresso.
Consideraciones
- En una app que se comporta correctamente, todas las vistas con las que un usuario puede interactuar deben incluir texto descriptivo o tener una descripción del contenido. Consulta Cómo hacer que las apps sean más accesibles para obtener más información. Si no puedes acotar una búsqueda con
withText()
owithContentDescription()
, procura tratarla como un error de accesibilidad. - Usa el comparador menos descriptivo que encuentre la vista que estás buscando. No incluyas demasiadas especificaciones, ya que esto obligará al framework a realizar más trabajo del necesario. Por ejemplo, si una vista puede identificarse de forma única por su texto, no es necesario especificar que también se puede asignar desde
TextView
. Para muchas vistas, elR.id
de la vista debería ser suficiente. - Si la vista de destino está dentro de una
AdapterView
, comoListView
,GridView
oSpinner
, es posible que el métodoonView()
no funcione. En estos casos, debes usaronData()
.
Cómo ejecutar una acción en una vista
Cuando hayas encontrado un comparador adecuado para la vista de destino, podrás ejecutar instancias de ViewAction
en él con el método perform.
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 se mostrará la vista 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 vistas que proporciona Espresso.
Cómo verificar las aserciones de vistas
Es posible aplicar aserciones a la vista seleccionada con el método check()
. La aserción más utilizada es matches()
, que utiliza un objeto ViewMatcher
para confirmar el estado de la vista que se encuentra seleccionada.
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 esté visible una vista con el texto "Hello!"
(por ejemplo, después de un cambio en la marca de visibilidad de las vistas), el código anterior 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 en SimpleActivity
tiene un R.id
único, como se esperaba.
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 de 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 evitar ese problema, Espresso brinda un punto de entrada de onData()
independiente que puede cargar el elemento del adaptador en cuestión y ponerlo en foco antes de realizar operaciones con este o cualquiera de sus elementos secundarios.
Advertencia: Las implementaciones personalizadas de AdapterView
pueden tener problemas con el método onData()
si no respetan los contratos de herencia, especialmente la API de getItem()
. En esos casos, lo mejor es refactorizar el código de la aplicación. Si no puedes hacerlo, implementa un AdapterViewProtocol
personalizado de coincidencia. Para obtener más información, consulta la clase AdapterViewProtocols
predeterminada de 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 una 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 TextView
contenga el elemento. Como la clase de Spinner
se basa en una AdapterView
, se recomienda usar onData()
en lugar de onView()
para establecer la coincidencia con 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 se incluya en la jerarquía de vistas. Si usas onData()
, se fuerza la incorporación del elemento deseado en la jerarquía de vistas. Los elementos del Spinner
son strings, por lo que queremos establecer la coincidencia con 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 genera unaNoMatchingViewException
. Puedes examinar la jerarquía de vistas en la string de excepción con el fin de analizar por qué el comparador no identifica coincidencias con ninguna vista. - Si
onView()
encuentra varias vistas que coinciden con el comparador en cuestión, se genera unaAmbiguousViewMatcherException
. Se imprime la jerarquía de vistas y todas las vistas coincidentes se marcan con la etiquetaMATCHES
:
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, 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 de onView()
arroja una NoMatchingViewException
y hay widgets de AdapterView
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 cómo usar Espresso en las pruebas de Android, consulta los siguientes recursos.
Muestras
- CustomMatcherSample: muestra cómo extender Espresso para que coincida con la propiedad de sugerencia de un objeto
EditText
- RecyclerViewSample: acciones de
RecyclerView
para Espresso - (Más…)