Prueba el componente Navigation

1. Antes de comenzar

En codelabs anteriores, aprendiste sobre la navegación con el componente Navigation. En este codelab, aprenderás a probar este componente. Ten en cuenta que esto es diferente de probar la navegación sin usar un componente de navegación.

Requisitos previos

  • Haber creado directorios de prueba en Android Studio
  • Haber escrito pruebas de instrumentación y de unidades en Android Studio
  • Haber agregado dependencias de Gradle a un proyecto de Android

Qué aprenderás

  • Cómo usar pruebas de instrumentación para probar los componentes de navegación.
  • Cómo configurar pruebas sin código repetido.

Requisitos

  • Una computadora que tenga Android Studio instalado
  • El código de solución de la app de Words

Descarga el código de partida para este codelab

En este codelab, agregarás pruebas de instrumentación al código de solución para la app de Words.

  1. Navega a la página de repositorio de GitHub del proyecto.
  2. Verifica que el nombre de la rama coincida con el especificado en el codelab. Por ejemplo, en la siguiente captura de pantalla, el nombre de la rama es main.

1e4c0d2c081a8fd2.png

  1. En la página de GitHub de este proyecto, haz clic en el botón Code, el cual abre una ventana emergente.

1debcf330fd04c7b.png

  1. En la ventana emergente, haz clic en el botón Download ZIP para guardar el proyecto en tu computadora. Espera a que se complete la descarga.
  2. Ubica el archivo en tu computadora (probablemente en la carpeta Descargas).
  3. Haz doble clic en el archivo ZIP para descomprimirlo. Se creará una carpeta nueva con los archivos del proyecto.

Abre el proyecto en Android Studio

  1. Inicia Android Studio.
  2. En la ventana Welcome to Android Studio, haz clic en Open.

d8e9dbdeafe9038a.png

Nota: Si Android Studio ya está abierto, selecciona la opción de menú File > Open.

8d1fda7396afe8e5.png

  1. En el navegador de archivos, ve hasta donde se encuentra la carpeta del proyecto descomprimida (probablemente en Descargas).
  2. Haz doble clic en la carpeta del proyecto.
  3. Espera a que Android Studio abra el proyecto.
  4. Haz clic en el botón Run 8de56cba7583251f.png para compilar y ejecutar la app. Asegúrate de que funcione como se espera.

2. Descripción general de la app de partida

La app de Words consiste en una pantalla principal que muestra una lista en la que cada elemento es una letra del alfabeto. Si haces clic en una letra, navegarás a una pantalla que muestra una lista de palabras que comienzan con esa letra.

3. Crea los directorios de prueba

Si es necesario, crea un directorio de prueba de instrumentación para la app de Words, como lo hiciste en codelabs anteriores. Si ya lo hiciste, puedes omitir el paso y dirigirte hasta Agrega las dependencias necesarias.

4. Crea una clase de prueba de instrumentación

Crea una clase nueva llamada NavigationTests.kt en la carpeta androidTest.

b023023a2ccc3813.png

5. Agrega las dependencias necesarias

Para probar el componente de navegación, se requieren algunas dependencias específicas de Gradle. También incluiremos una dependencia que nos permita probar fragmentos de una manera muy específica. Navega hasta el archivo build.gradle del módulo de tu app y agrega la siguiente dependencia:

androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
androidTestImplementation 'com.android.support.test.espresso:espresso-contrib:3.4.0'
androidTestImplementation 'androidx.navigation:navigation-testing:2.5.2'

debugImplementation 'androidx.fragment:fragment-testing:1.5.3'

Ahora, sincroniza tu proyecto.

6. Escribe la prueba del componente de Navigation

Probar el componente Navigation difiere de probar la navegación normal. Cuando probamos la navegación normal, esta se activa para ejecutarse en el dispositivo o emulador. Cuando probamos el componente Navigation, no hacemos que el dispositivo o el emulador sean visibles para la navegación. En su lugar, forzamos al controlador de navegación a navegar sin que realmente cambie lo que se muestra en el dispositivo o emulador. Luego, comprobamos que el controlador de navegación llega al destino correcto.

  1. Crea una función de prueba llamada navigate_to_words_nav_component().
  2. El trabajo con el componente Navigation en las pruebas requiere cierta configuración. En el método navigate_to_words_nav_component(), crea una instancia de prueba del controlador de navegación.
val navController = TestNavHostController(
   ApplicationProvider.getApplicationContext()
)
  1. El componente Navigation controla la IU mediante Fragments. Existe un fragmento equivalente a ActivityScenarioRule que se puede usar para aislar un fragmento con el objetivo de realizar pruebas. Es por eso que se requiere la dependencia específica del fragmento. Esto puede resultar muy útil para probar un fragmento que requiera mucha navegación, ya que se puede iniciar sin ningún código adicional para controlar la navegación hacia él.
val letterListScenario = launchFragmentInContainer<LetterListFragment>(themeResId =
R.style.Theme_Words)

Aquí, especificamos que queremos iniciar LetterListFragment. Debemos pasar el tema de la app a fin de que el componente de la IU sepa qué tema usar. De lo contrario, la prueba podría fallar.

  1. Por último, debemos declarar de forma explícita el gráfico de navegación que queremos que utilice el controlador de navegación para el fragmento lanzado.
letterListScenario.onFragment { fragment ->

   navController.setGraph(R.navigation.nav_graph)

   Navigation.setViewNavController(fragment.requireView(), navController)
}
  1. Ahora, activa el evento que solicita la navegación.
onView(withId(R.id.recycler_view))
   .perform(RecyclerViewActions
       .actionOnItemAtPosition<RecyclerView.ViewHolder>(2, click()))

Cuando se usa el método launchFragmentInContainer(), no es posible la navegación real porque el contenedor no conoce otros fragmentos ni actividades a las que podamos navegar. Solo conoce el fragmento que especificamos para el lanzamiento en él. Por lo tanto, cuando ejecutes esta prueba en un dispositivo o emulador, no verás ninguna navegación real. Esto puede parecer poco intuitivo, pero nos permite hacer una aserción mucho más directa con respecto al destino actual. En lugar de buscar un componente de la IU que sabemos que se muestra en una pantalla en particular, podemos verificar que el destino del controlador de navegación actual tenga el ID del fragmento en el que esperamos estar. Este enfoque es significativamente más confiable que el mencionado anteriormente.

assertEquals(navController.currentDestination?.id, R.id.wordListFragment)

La prueba debería verse de la siguiente manera: 78b4a72f75134ded.png

7. Código de solución

8. Cómo evitar la repetición de código con anotaciones

En Android, tanto las pruebas de instrumentación como las de unidad tienen una función que nos permite establecer la misma configuración para cada prueba de una clase sin código repetitivo.

Supongamos, por ejemplo, que teníamos un fragmento con 10 botones. Cada botón conduce a un fragmento único cuando se hace clic en él.

Si seguimos el patrón de la prueba anterior, tendríamos que repetir el código como este para cada una de las 10 pruebas (ten en cuenta que este código es estrictamente un ejemplo y no se compilará en la app con la que trabajamos en este codelab):

val navController = TestNavHostController(
    ApplicationProvider.getApplicationContext()
)

val exampleFragmentScenario = launchFragmentInContainer<ExampleFragment>(themeResId =
R.style.Theme_Example)

exampleFragmentScenario.onFragment { fragment ->

   navController.setGraph(R.navigation.example_nav_graph)

   Navigation.setViewNavController(fragment.requireView(), navController)
}

Es mucho código para repetir 10 veces. Sin embargo, en este caso, podemos ahorrarnos algo de tiempo si usamos la anotación @Before que proporciona JUnit. Para ello, anotamos un método en el que proporcionamos el código necesario para configurar nuestra prueba. Podemos poner cualquier nombre al método, pero debe ser relevante. En lugar de configurar el mismo fragmento 10 veces, podemos escribir el código de configuración de esta manera:

lateinit var navController: TestNavHostController

lateinit var exampleFragmentScenario: FragmentScenario<ExampleFragment>

@Before
fun setup(){
    navController = TestNavHostController(
        ApplicationProvider.getApplicationContext()
    )

    exampleFragmentScenario =  launchFragmentInContainer(themeResId=R.style.Theme_Example)

    exampleFragmentScenario.onFragment { fragment ->

       navController.setGraph(R.navigation.example_nav_graph)

       Navigation.setViewNavController(fragment.requireView(),  navController)
    }
}

Este método se ejecuta para cada prueba que escribimos en esta clase, y podemos acceder a las variables necesarias desde cualquiera de las pruebas.

De manera similar, si hay código que debemos ejecutar después de cada prueba, podemos usar la anotación @After. Por ejemplo, se puede usar @After a fin de borrar un recurso que usamos para nuestra prueba o, en el caso de las pruebas de instrumentación, podemos usarlo para hacer que el dispositivo cambie a un estado específico.

JUnit también proporciona las anotaciones @BeforeClass y @AfterClass. La diferencia consiste en que los métodos con esta anotación se ejecutan una vez, pero el código ejecutado se aplica a todos los métodos. Si tus métodos de configuración o desplazamiento contienen operaciones costosas, es preferible usar estas anotaciones en su lugar. Los métodos con las anotaciones @BeforeClass y @AfterClass deben colocarse en un objeto complementario y anotarse con @JvmStatic. Para demostrar el orden de ejecución de estas anotaciones, veamos el siguiente código:

5157ab00a9b7fb84.png

Recuerda que @BeforeClass se ejecuta para la clase, @Before se ejecuta antes que las funciones, @After se ejecuta después de las funciones y @AfterClass se ejecuta para la clase. ¿Puedes predecir cómo se vería el resultado?

39c04aa2ba7b8348.png

El orden de ejecución de las funciones es setupClass(), setupFunction(), test_a(), tearDownFunction(), setupFunction(), test_b(), tearDownFunction(), setupFunction(), test_c(), tearDownFunction(), tearDownClass(). Esto tiene sentido porque @Before y @After se ejecutan antes y después de cada método, respectivamente. @BeforeClass se ejecuta una vez antes de que se ejecute cualquier elemento de la clase, y @AfterClass se ejecuta una vez después de que todo lo demás se haya ejecutado.

9. Felicitaciones

En este codelab, lograste lo siguiente:

  • Probar el componente Navigation
  • Evitar el código repetitivo con las anotaciones @Before, @BeforeClass, @After y @AfterClass