Prueba la IU para una sola app

Probar las interacciones de usuario dentro de una sola app ayuda a garantizar que los usuarios no tengan resultados inesperados ni una mala experiencia cuando interactúen con la app. Deberías adquirir el hábito de crear pruebas de interfaz de usuario (IU) si necesitas verificar que la IU de la app funcione correctamente.

El framework de Espresso, entregado por AndroidX Test, proporciona API para escribir pruebas de IU que simulan las interacciones del usuario en una sola app de destino. Las pruebas de Espresso se pueden ejecutar en dispositivos con Android 2.3.3 (API nivel 10) o superior. Una ventaja clave de usar Espresso es que proporciona la sincronización automática de las acciones de prueba con la IU de la app en cuestión. Espresso detecta cuando el subproceso principal está inactivo, de modo que puede ejecutar los comandos de prueba en el momento adecuado y mejorar la confiabilidad de las pruebas. Con esta capacidad, no se necesita agregar ninguna solución de tiempo, como Thread.sleep(), en el código de prueba.

El framework de prueba de Espresso es una API basada en instrumentación y funciona con el panel de prueba AndroidJUnitRunner.

Cómo configurar Espresso

Antes de compilar la prueba de la IU con Espresso, asegúrate de establecer una referencia de dependencia a la biblioteca de Espresso:

    dependencies {
        androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0'
    }
    

Desactiva las animaciones en el dispositivo de prueba. Si dejas activadas las animaciones del sistema en el dispositivo de prueba, es posible que se produzcan resultados inesperados o que la prueba falle. Para desactivar las animaciones, accede a Configuración, abre Opciones para desarrolladores y desactiva todas las siguientes opciones:

  • Escala de animación de ventana
  • Escala de animación de transición
  • Escala de duración de animador

Si quieres configurar el proyecto para usar funciones de Espresso distintas que las de la API principal, consulta las guías específicas de Espresso.

Cómo crear una clase de prueba de Espresso

Para crear una prueba de Espresso, sigue este modelo de programación:

  1. Llama a los métodos onView() o onData() de los controles AdapterView para buscar el componente de IU que quieres probar en una Activity (por ejemplo, un botón de acceso en la app).
  2. Llama a los métodos ViewInteraction.perform() o DataInteraction.perform(), y pasa la acción del usuario (por ejemplo, un clic en el botón de acceso) para simular una interacción específica del usuario por realizar con el componente de la IU. Para secuenciar varias acciones en el mismo componente de la IU, encadénalas con una lista separada por comas en el argumento del método.
  3. Repite los pasos anteriores según sea necesario, con el fin de simular el flujo de un usuario a través de varias actividades en la app de destino.
  4. Usa los métodos ViewAssertions para comprobar que la IU refleje el estado o el comportamiento esperado después de que se realicen esas interacciones de usuario.

Estos pasos se explican con más detalles en las siguientes secciones.

En el siguiente fragmento de código, se muestra cómo la clase de prueba puede invocar este flujo de trabajo básico:

Kotlin

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

Java

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

Cómo usar Espresso con ActivityTestRule

En esta sección, se describe cómo crear una prueba de Espresso nueva con el estilo JUnit 4 y usar ActivityTestRule para reducir la cantidad de código estándar que debes escribir. Con ActivityTestRule, el framework de prueba inicia la actividad en cuestión antes de cada método de prueba anotado con @Test y antes de cualquier método anotado con @Before. El framework controla el cierre de la actividad después de que finaliza la prueba y se ejecutan todos los métodos con la anotación @After.

Kotlin

    package com.example.android.testing.espresso.BasicSample

    import org.junit.Before
    import org.junit.Rule
    import org.junit.Test
    import org.junit.runner.RunWith

    import androidx.test.rule.ActivityTestRule
    import androidx.test.runner.AndroidJUnit4

    @RunWith(AndroidJUnit4::class)
    @LargeTest
    class ChangeTextBehaviorTest {

        private lateinit var stringToBetyped: String

        @get:Rule
        var activityRule: ActivityTestRule<MainActivity>
                = ActivityTestRule(MainActivity::class.java)

        @Before
        fun initValidString() {
            // Specify a valid string.
            stringToBetyped = "Espresso"
        }

        @Test
        fun changeText_sameActivity() {
            // Type text and then press the button.
            onView(withId(R.id.editTextUserInput))
                    .perform(typeText(stringToBetyped), closeSoftKeyboard())
            onView(withId(R.id.changeTextBt)).perform(click())

            // Check that the text was changed.
            onView(withId(R.id.textToBeChanged))
                    .check(matches(withText(stringToBetyped)))
        }
    }
    

Java

    package com.example.android.testing.espresso.BasicSample;

    import org.junit.Before;
    import org.junit.Rule;
    import org.junit.Test;
    import org.junit.runner.RunWith;

    import androidx.test.rule.ActivityTestRule;
    import androidx.test.runner.AndroidJUnit4;

    @RunWith(AndroidJUnit4.class)
    @LargeTest
    public class ChangeTextBehaviorTest {

        private String stringToBetyped;

        @Rule
        public ActivityTestRule<MainActivity> activityRule
                = new ActivityTestRule<>(MainActivity.class);

        @Before
        public void initValidString() {
            // Specify a valid string.
            stringToBetyped = "Espresso";
        }

        @Test
        public void changeText_sameActivity() {
            // Type text and then press the button.
            onView(withId(R.id.editTextUserInput))
                    .perform(typeText(stringToBetyped), closeSoftKeyboard());
            onView(withId(R.id.changeTextBt)).perform(click());

            // Check that the text was changed.
            onView(withId(R.id.textToBeChanged))
                    .check(matches(withText(stringToBetyped)));
        }
    }
    

Cómo acceder a los componentes de la IU

Antes de que Espresso pueda interactuar con la app en evaluación, primero debes especificar el componente o la vista de la IU. Espresso admite el uso de comparadores Hamcrest para especificar las vistas y los adaptadores en la app.

Para encontrar la vista, llama al método onView() y pasa un comparador de vistas que especifique la vista a la que apuntas. Este método se describe detalladamente en Cómo especificar un comparador de vistas. El método onView() muestra un objeto ViewInteraction que permite que la prueba interactúe con la vista. Sin embargo, es posible que la llamada al método onView() no funcione si quieres ubicar una vista en un diseño RecyclerView. En este caso, sigue las instrucciones de Cómo localizar una vista en AdapterView.

Nota: El método onView() no comprueba si es válida la vista especificada. En su lugar, Espresso busca solo en la jerarquía de vistas actual con el comparador proporcionado. Si no se encuentra ninguna coincidencia, el método arroja una NoMatchingViewException.

En el siguiente fragmento de código, se muestra cómo escribir una prueba que accede a un campo EditText, ingresa una string de texto, cierra el teclado virtual y luego hace clic en un botón.

Kotlin

    fun testChangeText_sameActivity() {
        // Type text and then press the button.
        onView(withId(R.id.editTextUserInput))
                .perform(typeText(STRING_TO_BE_TYPED), closeSoftKeyboard())
        onView(withId(R.id.changeTextButton)).perform(click())

        // Check that the text was changed.
        ...
    }
    

Java

    public void testChangeText_sameActivity() {
        // Type text and then press the button.
        onView(withId(R.id.editTextUserInput))
                .perform(typeText(STRING_TO_BE_TYPED), closeSoftKeyboard());
        onView(withId(R.id.changeTextButton)).perform(click());

        // Check that the text was changed.
        ...
    }
    

Especifica un comparador de vistas

Puedes especificar un comparador de vistas con estos enfoques:

  • Llamar a métodos de la clase ViewMatchers. Por ejemplo, para encontrar una vista mediante la búsqueda de una string de texto que se muestra, puedes llamar a un método como el siguiente:

    Kotlin

        onView(withText("Sign-in"))
        

    Java

        onView(withText("Sign-in"));
        

    También puedes llamar a withId() y proporcionar el ID de recurso (R.id) de la vista, como se muestra en el siguiente ejemplo:

    Kotlin

        onView(withId(R.id.button_signin))
        

    Java

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

    No se garantiza que los ID de recursos de Android sean únicos. Si la prueba intenta coincidir con un ID de recurso usado por más de una vista, Espresso arrojará un AmbiguousViewMatcherException.

  • Usar la clase Matchers de Hamcrest. Puedes utilizar los métodos allOf() para combinar varios comparadores, como containsString() y instanceOf(). Este enfoque permite filtrar aún más los resultados de las coincidencias, como se muestra en el siguiente ejemplo:

    Kotlin

        onView(allOf(withId(R.id.button_signin), withText("Sign-in")))
        

    Java

        onView(allOf(withId(R.id.button_signin), withText("Sign-in")));
        

    Puedes usar la palabra clave not para filtrar las vistas que no correspondan al comparador, como se muestra en el siguiente ejemplo:

    Kotlin

        onView(allOf(withId(R.id.button_signin), not(withText("Sign-out"))))
        

    Java

        onView(allOf(withId(R.id.button_signin), not(withText("Sign-out"))));
        

    Para usar estos métodos en la prueba, importa el paquete org.hamcrest.Matchers. Para obtener más información sobre las coincidencias de Hamcrest, consulta el sitio de Hamcrest.

A fin de mejorar el rendimiento de las pruebas de Espresso, especifica la información de coincidencia mínima necesaria para encontrar la vista de destino. Por ejemplo, si se puede identificar una vista de forma única por el texto descriptivo, no es necesario especificar que también se puede asignar desde la instancia TextView.

Cómo localizar una vista en AdapterView

En un widget AdapterView, la vista se propaga de forma dinámica con vistas secundarias durante el tiempo de ejecución. Si la vista de destino que quieres probar está dentro de un AdapterView (como ListView, GridView o Spinner), el método onView() podría no funcionar porque solo se puede cargar un subconjunto de las vistas en la jerarquía de vistas actual.

Por el contrario, llama al método onData() para obtener un objeto DataInteraction a fin de acceder al elemento de vista de destino. Espresso controla la carga del elemento de vista de destino en la jerarquía de vistas actual y el desplazamiento hasta el elemento de destino y el enfoque del elemento.

Nota: El método onData() no comprueba si el elemento especificado coincide con una vista. Espresso busca solo la jerarquía de vistas actual. Si no se encuentra ninguna coincidencia, el método arroja una NoMatchingViewException.

En el siguiente fragmento de código, se muestra cómo usar el método onData() junto con la coincidencia de Hamcrest para buscar una fila específica en una lista que contiene una string determinada. En este ejemplo, la clase LongListActivity contiene una lista de strings expuestas a través de un SimpleAdapter.

Kotlin

    onData(allOf(`is`(instanceOf(Map::class.java)),
            hasEntry(equalTo(LongListActivity.ROW_TEXT),
            `is`("test input"))))
    

Java

    onData(allOf(is(instanceOf(Map.class)),
            hasEntry(equalTo(LongListActivity.ROW_TEXT), is("test input"))));
    

Cómo realizar acciones

Llama a los métodos ViewInteraction.perform() o DataInteraction.perform() para simular las interacciones del usuario con el componente de IU. Debes pasar uno o más objetos ViewAction como argumentos. Espresso activa cada acción en secuencia según el orden específico y las ejecuta en el subproceso principal.

La clase ViewActions proporciona una lista de métodos de ayuda para especificar acciones comunes. Puedes usar esos métodos como accesos directos prácticos en lugar de crear y configurar objetos individuales de ViewAction. Puedes especificar acciones como las siguientes:

Si la vista de destino está dentro de una ScrollView, primero realiza la acción ViewActions.scrollTo() para mostrar la vista en la pantalla antes de continuar con otras acciones. La acción ViewActions.scrollTo() no tendrá ningún efecto si ya se muestra la vista.

Cómo probar las actividades en aislamiento con Espresso Intents

Espresso Intents permite la validación y el stubbing de los intents enviados por una app. Con Espresso Intents, puedes interceptar los intents salientes, hacer un stub del resultado y a enviarlo de vuelta al componente en cuestión para probar una app, una actividad o un servicio de forma aislada.

Para comenzar las pruebas con Espresso Intents, debes agregar la siguiente línea al archivo build.gradle de la app:

    dependencies {
      androidTestImplementation 'androidx.test.espresso:espresso-intents:3.1.0'
    }
    

Para probar un intent, debes crear una instancia de la clase IntentsTestRule, que es muy similar a la clase ActivityTestRule. La clase IntentsTestRule inicializa Espresso Intents antes de cada prueba, termina la actividad del host y libera Espresso Intents después de cada prueba.

La clase de prueba que se muestra en el siguiente fragmento de código proporciona una prueba simple para un intent explícito, y prueba las actividades y los intents creados en el tutorial Cómo compilar tu primera app.

Kotlin

    private const val MESSAGE = "This is a test"
    private const val PACKAGE_NAME = "com.example.myfirstapp"

    @RunWith(AndroidJUnit4::class)
    class SimpleIntentTest {

        /* Instantiate an IntentsTestRule object. */
        @get:Rule
        var intentsRule: IntentsTestRule<MainActivity> = IntentsTestRule(MainActivity::class.java)

        @Test
        fun verifyMessageSentToMessageActivity() {

            // Types a message into a EditText element.
            onView(withId(R.id.edit_message))
                    .perform(typeText(MESSAGE), closeSoftKeyboard())

            // Clicks a button to send the message to another
            // activity through an explicit intent.
            onView(withId(R.id.send_message)).perform(click())

            // Verifies that the DisplayMessageActivity received an intent
            // with the correct package name and message.
            intended(allOf(
                    hasComponent(hasShortClassName(".DisplayMessageActivity")),
                    toPackage(PACKAGE_NAME),
                    hasExtra(MainActivity.EXTRA_MESSAGE, MESSAGE)))

        }
    }
    

Java

    @Large
    @RunWith(AndroidJUnit4.class)
    public class SimpleIntentTest {

        private static final String MESSAGE = "This is a test";
        private static final String PACKAGE_NAME = "com.example.myfirstapp";

        /* Instantiate an IntentsTestRule object. */
        @Rule
        public IntentsTestRule<MainActivity> intentsRule =
                new IntentsTestRule<>(MainActivity.class);

        @Test
        public void verifyMessageSentToMessageActivity() {

            // Types a message into a EditText element.
            onView(withId(R.id.edit_message))
                    .perform(typeText(MESSAGE), closeSoftKeyboard());

            // Clicks a button to send the message to another
            // activity through an explicit intent.
            onView(withId(R.id.send_message)).perform(click());

            // Verifies that the DisplayMessageActivity received an intent
            // with the correct package name and message.
            intended(allOf(
                    hasComponent(hasShortClassName(".DisplayMessageActivity")),
                    toPackage(PACKAGE_NAME),
                    hasExtra(MainActivity.EXTRA_MESSAGE, MESSAGE)));

        }
    }
    

Para obtener más información acerca de Espresso Intents, consulta la documentación de Espresso Intents disponible en el sitio de AndroidX Test. También puedes descargar las muestras de código IntentsBasicSample y IntentsAdvancedSample.

Cómo hacer pruebas WebViews con Espresso Web

Espresso Web permite probar los componentes WebView contenidos en una actividad. Usa la API de WebDriver para inspeccionar y controlar el comportamiento de una WebView.

Para realizar pruebas con Espresso Web, debes agregar la siguiente línea al archivo build.gradle de la app:

    dependencies {
      androidTestImplementation 'androidx.test.espresso:espresso-web:3.1.0'
    }
    

Cuando creas una prueba con Espresso Web, debes habilitar JavaScript en WebView al momento de creación de una instancia del objeto ActivityTestRule para probar la actividad. En las pruebas, puedes seleccionar elementos HTML que se muestran en WebView y simular las interacciones del usuario, como ingresar texto en un cuadro de texto y luego hacer clic en un botón. Una vez completadas las acciones, puedes verificar si los resultados de la página web coinciden con los resultados esperados.

En el siguiente fragmento de código, la clase prueba un componente WebView con el valor de ID "webview" en la actividad que se está probando. La prueba typeTextInInput_clickButton_SubmitsForm() selecciona un elemento <input> en la página web, ingresa texto y verifica el texto que aparece en otro elemento.

Kotlin

    private const val MACCHIATO = "Macchiato"
    private const val DOPPIO = "Doppio"

    @LargeTest
    @RunWith(AndroidJUnit4::class)
    class WebViewActivityTest {

        @get:Rule
        val activityRule = object : ActivityTestRule<WebViewActivity>(
                WebViewActivity::class.java,
                false,      /* Initial touch mode */
                false       /* launch activity */
        ) {
            override fun afterActivityLaunched() {
                // Enable JavaScript.
                onWebView().forceJavascriptEnabled()
            }
        }

        @Test
        fun typeTextInInput_clickButton_SubmitsForm() {
            // Lazily launch the Activity with a custom start Intent per test
            activityRule.launchActivity(withWebFormIntent())

            // Selects the WebView in your layout.
            // If you have multiple WebViews you can also use a
            // matcher to select a given WebView, onWebView(withId(R.id.web_view)).
            onWebView()
                    // Find the input element by ID
                    .withElement(findElement(Locator.ID, "text_input"))
                    // Clear previous input
                    .perform(clearElement())
                    // Enter text into the input element
                    .perform(DriverAtoms.webKeys(MACCHIATO))
                    // Find the submit button
                    .withElement(findElement(Locator.ID, "submitBtn"))
                    // Simulate a click via JavaScript
                    .perform(webClick())
                    // Find the response element by ID
                    .withElement(findElement(Locator.ID, "response"))
                    // Verify that the response page contains the entered text
                    .check(webMatches(getText(), containsString(MACCHIATO)))
        }
    }
    

Java

    @LargeTest
    @RunWith(AndroidJUnit4.class)
    public class WebViewActivityTest {

        private static final String MACCHIATO = "Macchiato";
        private static final String DOPPIO = "Doppio";

        @Rule
        public ActivityTestRule<WebViewActivity> activityRule =
            new ActivityTestRule<WebViewActivity>(WebViewActivity.class,
                false /* Initial touch mode */, false /*  launch activity */) {

            @Override
            protected void afterActivityLaunched() {
                // Enable JavaScript.
                onWebView().forceJavascriptEnabled();
            }
        }

        @Test
        public void typeTextInInput_clickButton_SubmitsForm() {
           // Lazily launch the Activity with a custom start Intent per test
           activityRule.launchActivity(withWebFormIntent());

           // Selects the WebView in your layout.
           // If you have multiple WebViews you can also use a
           // matcher to select a given WebView, onWebView(withId(R.id.web_view)).
           onWebView()
               // Find the input element by ID
               .withElement(findElement(Locator.ID, "text_input"))
               // Clear previous input
               .perform(clearElement())
               // Enter text into the input element
               .perform(DriverAtoms.webKeys(MACCHIATO))
               // Find the submit button
               .withElement(findElement(Locator.ID, "submitBtn"))
               // Simulate a click via JavaScript
               .perform(webClick())
               // Find the response element by ID
               .withElement(findElement(Locator.ID, "response"))
               // Verify that the response page contains the entered text
               .check(webMatches(getText(), containsString(MACCHIATO)));
        }
    }
    

Para obtener más información sobre Espresso Web, consulta la documentación de Espresso Web en el sitio de AndroidX Test. También puedes descargar este fragmento de código como parte de la muestra de código de Espresso Web.

Cómo verificar los resultados

Llama a los métodos ViewInteraction.check() o DataInteraction.check() para afirmar que la vista de la IU coincide con algún estado esperado. Debes pasar un objeto ViewAssertion como argumento. Si falla la afirmación, Espresso arroja un AssertionFailedError.

La clase ViewAssertions proporciona una lista de métodos de ayuda para especificar afirmaciones comunes. Estas son algunas de las afirmaciones que puedes usar:

  • doesNotExist: afirma que ninguna vista coincide con los criterios especificados en la jerarquía de vistas actual.
  • matches: afirma que la vista especificada existe en la jerarquía de vistas actual y que el estado coincide con un comparador de Hamcrest determinado.
  • selectedDescendentsMatch: afirma que existen las vistas secundarias especificadas para una vista superior y que el estado coincide con algún comparador de Hamcrest determinado.

En el siguiente fragmento de código, se muestra cómo comprobar que el texto que se muestra en la IU tenga el mismo valor que el texto ingresado previamente en el campo EditText.

Kotlin

    fun testChangeText_sameActivity() {
        // Type text and then press the button.
        ...

        // Check that the text was changed.
        onView(withId(R.id.textToBeChanged))
                .check(matches(withText(STRING_TO_BE_TYPED)))
    }
    

Java

    public void testChangeText_sameActivity() {
        // Type text and then press the button.
        ...

        // Check that the text was changed.
        onView(withId(R.id.textToBeChanged))
                .check(matches(withText(STRING_TO_BE_TYPED)));
    }
    

Cómo ejecutar pruebas de Espresso en un dispositivo o emulador

Puedes ejecutar pruebas de Espresso desde Android Studio o desde la línea de comandos. Asegúrate de especificar AndroidJUnitRunner como el comparador de instrumentación predeterminado en el proyecto.

A fin de ejecutar la prueba de Espresso, sigue los pasos de ejecución de las pruebas de instrumentación que se describen en Cómo comenzar con las pruebas.

También debes leer la Referencia de la API de Espresso.

Recursos adicionales

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

Ejemplos

Codelabs