Conceptos básicos de Espresso

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() y onData()). También expone las API que no están necesariamente vinculadas a ninguna vista, como pressBack().
  • ViewMatchers: 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: colección de objetos ViewAction que se pueden pasar al método ViewInteraction.perform(), como click().
  • ViewAssertions: 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 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() o withContentDescription(), 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, 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 estos 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 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 una NoMatchingViewException. 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 una AmbiguousViewMatcherException. Se imprime 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, 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