Conceptos básicos de las pruebas

Este codelab es parte del curso Aspectos avanzados de Android en Kotlin. Aprovecharás al máximo este curso si trabajas con los codelabs de forma secuencial, aunque no es obligatorio. Todos los codelabs del curso se indican en la página de destino de los codelabs de Aspectos avanzados de Android en Kotlin.

Introducción

Cuando implementaste la primera función de tu primera app, es probable que hayas ejecutado el código para verificar que funcionara según lo previsto. Realizaste una prueba, aunque fue una prueba manual. A medida que agregabas y actualizabas funciones, probablemente también ejecutabas el código y verificabas que funcionara. Sin embargo, hacer esto manualmente cada vez es agotador, propenso a errores y no se puede escalar.

Las computadoras son excelentes para el escalamiento y la automatización. Por lo tanto, los desarrolladores de empresas grandes y pequeñas escriben pruebas automatizadas, que son pruebas que ejecuta el software y no requieren que operes la app de forma manual para verificar que el código funcione.

En esta serie de codelabs, aprenderás a crear una colección de pruebas (conocida como paquete de pruebas) para una app del mundo real.

En este primer codelab, se abarcan los conceptos básicos de las pruebas en Android. Escribirás tus primeras pruebas y aprenderás a probar LiveData y ViewModel.

Conocimientos que ya deberías tener

Debes estar familiarizado con lo siguiente:

Qué aprenderás

Aprenderás sobre los siguientes temas:

  • Cómo escribir y ejecutar pruebas de unidades en Android
  • Cómo usar el desarrollo basado en pruebas
  • Cómo elegir pruebas instrumentadas y pruebas locales

Aprenderás sobre las siguientes bibliotecas y conceptos de código:

Actividades

  • Configurar, ejecutar e interpretar pruebas locales y de instrumentación en Android
  • Escribir pruebas de unidades en Android con JUnit4 y Hamcrest
  • Escribe pruebas simples de LiveData y ViewModel.

En esta serie de codelabs, trabajarás con la app de notas de tareas pendientes. Esta app te permite escribir tareas para completar y mostrarlas en una lista. Luego, puedes marcarlas como completadas o no, filtrarlas o borrarlas.

Esta app está escrita en Kotlin, tiene varias pantallas, usa componentes de Jetpack y sigue la arquitectura de una Guía de arquitectura de apps. Si aprendes a probar esta app, podrás probar apps que usen las mismas bibliotecas y arquitectura.

Para comenzar, descarga el código:

Download Zip

Como alternativa, puedes clonar el repositorio de GitHub para el código:

$ git clone https://github.com/googlecodelabs/android-testing.git
$ cd android-testing
$ git checkout starter_code

En esta tarea, ejecutarás la app y explorarás la base de código.

Paso 1: Ejecuta la app de ejemplo

Una vez que hayas descargado la app de tareas, ábrela en Android Studio y ejecútala. Debería compilarse. Explora la app haciendo lo siguiente:

  • Crea una tarea nueva con el botón de acción flotante de signo más. Primero, ingresa un título y, luego, información adicional sobre la tarea. Guárdalo con el botón de acción flotante de marca de verificación verde.
  • En la lista de tareas, haz clic en el título de la tarea que acabas de completar y mira la pantalla de detalles para ver el resto de la descripción.
  • En la lista o en la pantalla de detalles, marca la casilla de verificación de esa tarea para establecer su estado como Completada.
  • Vuelve a la pantalla de tareas, abre el menú de filtros y filtra las tareas por estado Activa y Completada.
  • Abre el panel lateral de navegación y haz clic en Estadísticas.
  • Regresa a la pantalla de descripción general y, en el menú del panel de navegación, selecciona Borrar completadas para borrar todas las tareas con el estado Completada.

Paso 2: Explora el código de la app de ejemplo

La app de tareas pendientes se basa en la popular muestra de pruebas y arquitectura de Architecture Blueprints (con la versión de arquitectura reactiva de la muestra). La app sigue la arquitectura de una Guía de arquitectura de apps. Usa ViewModels con Fragments, un repositorio y Room. Si conoces alguno de los siguientes ejemplos, esta app tiene una arquitectura similar:

Es más importante que comprendas la arquitectura general de la app que tener un conocimiento profundo de la lógica en cualquier capa.

Aquí tienes el resumen de los paquetes que encontrarás:

Paquete: com.example.android.architecture.blueprints.todoapp

.addedittask

Pantalla para agregar o editar una tarea: Código de la capa de la IU para agregar o editar una tarea.

.data

La capa de datos: Se ocupa de la capa de datos de las tareas. Contiene el código de la base de datos, la red y el repositorio.

.statistics

La pantalla de estadísticas: Código de la capa de la IU para la pantalla de estadísticas.

.taskdetail

La pantalla de detalles de la tarea: Código de la capa de IU para una sola tarea.

.tasks

La pantalla de tareas: Código de la capa de IU para la lista de todas las tareas.

.util

Clases de utilidad: Clases compartidas que se usan en varias partes de la app, p.ej., para el diseño de actualización por deslizamiento que se usa en varias pantallas.

Capa de datos (.data)

Esta app incluye una capa de red simulada, en el paquete remote, y una capa de base de datos, en el paquete local. Para simplificar, en este proyecto, la capa de redes se simula con solo un HashMap con una demora, en lugar de realizar solicitudes de red reales.

El DefaultTasksRepository coordina o media entre la capa de red y la capa de la base de datos, y es lo que devuelve datos a la capa de la IU.

Capa de la IU ( .addedittask, .statistics, .taskdetail, .tasks)

Cada uno de los paquetes de la capa de IU contiene un fragmento y un modelo de vista, junto con cualquier otra clase que se requiera para la IU (como un adaptador para la lista de tareas). TaskActivity es la actividad que contiene todos los fragmentos.

Navegación

El componente Navigation controla la navegación de la app. Se define en el archivo nav_graph.xml. La navegación se activa en los modelos de vista con la clase Event. Los modelos de vista también determinan qué argumentos pasar. Los fragmentos observan los Event y realizan la navegación real entre pantallas.

En esta tarea, ejecutarás tus primeras pruebas.

  1. En Android Studio, abre el panel Project y busca estas tres carpetas:
  • com.example.android.architecture.blueprints.todoapp
  • com.example.android.architecture.blueprints.todoapp (androidTest)
  • com.example.android.architecture.blueprints.todoapp (test)

Estas carpetas se conocen como conjuntos de fuentes. Los conjuntos de fuentes son carpetas que contienen el código fuente de tu app. Los conjuntos de fuentes, que se muestran en color verde (androidTest y test), contienen tus pruebas. Cuando creas un proyecto nuevo de Android, obtienes los siguientes tres conjuntos de orígenes de forma predeterminada. Son los siguientes:

  • main: Contiene el código de tu app. Este código se comparte entre todas las versiones diferentes de la app que puedes compilar (conocidas como variantes de compilación).
  • androidTest: Contiene pruebas conocidas como pruebas instrumentadas.
  • test: Contiene pruebas conocidas como pruebas locales.

La diferencia entre las pruebas locales y las pruebas instrumentadas radica en la forma en que se ejecutan.

Pruebas locales (conjunto de fuentes de test)

Estas pruebas se ejecutan de forma local en la JVM de tu máquina de desarrollo y no requieren un emulador ni un dispositivo físico. Por este motivo, se ejecutan rápido, pero su fidelidad es menor, lo que significa que actúan menos como lo harían en el mundo real.

En Android Studio, las pruebas locales se representan con un ícono de triángulo verde y rojo.

Pruebas instrumentadas (conjunto de fuentes androidTest)

Estas pruebas se ejecutan en dispositivos Android reales o emulados, por lo que reflejan lo que sucederá en el mundo real, pero también son mucho más lentas.

En Android Studio, las pruebas instrumentadas se representan con un ícono de Android con un triángulo verde y rojo.

Paso 1: Ejecuta una prueba local

  1. Abre la carpeta test hasta que encuentres el archivo ExampleUnitTest.kt.
  2. Haz clic con el botón derecho en él y selecciona Run ExampleUnitTest.

Deberías ver el siguiente resultado en la ventana Run en la parte inferior de la pantalla:

  1. Observa las marcas de verificación verdes y expande los resultados de la prueba para confirmar que se pasó una prueba llamada addition_isCorrect. Me alegra saber que la suma funciona como se esperaba.

Paso 2: Haz que falle la prueba

A continuación, se muestra la prueba que acabas de ejecutar.

ExampleUnitTest.kt

// A test class is just a normal class
class ExampleUnitTest {

   // Each test is annotated with @Test (this is a Junit annotation)
   @Test
   fun addition_isCorrect() {
       // Here you are checking that 4 is the same as 2+2
       assertEquals(4, 2 + 2)
   }
}

Ten en cuenta que las pruebas

  • Son una clase en uno de los conjuntos de orígenes de prueba.
  • Contienen funciones que comienzan con la anotación @Test (cada función es una sola prueba).
  • suelen contener instrucciones de aserción.

Android usa la biblioteca de pruebas JUnit para las pruebas (en este codelab, JUnit4). Tanto las aserciones como la anotación @Test provienen de JUnit.

Una aseveración es el núcleo de tu prueba. Es una instrucción de código que verifica que tu código o app se comporten según lo esperado. En este caso, la aserción es assertEquals(4, 2 + 2), que verifica que 4 sea igual a 2 + 2.

Para ver cómo se ve una prueba fallida, agrega una aserción que puedas ver fácilmente que debería fallar. Verificará que 3 sea igual a 1 + 1.

  1. Agrega assertEquals(3, 1 + 1) a la prueba addition_isCorrect.

ExampleUnitTest.kt

class ExampleUnitTest {

   // Each test is annotated with @Test (this is a Junit annotation)
   @Test
   fun addition_isCorrect() {
       assertEquals(4, 2 + 2)
       assertEquals(3, 1 + 1) // This should fail
   }
}
  1. Ejecuta la prueba.
  1. En los resultados de la prueba, observa una X junto a la prueba.

  1. También ten en cuenta lo siguiente:
  • Si falla una sola aserción, falla toda la prueba.
  • Se te indica el valor esperado (3) en comparación con el valor que se calculó en realidad (2).
  • Se te redireccionará a la línea de la aserción fallida (ExampleUnitTest.kt:16).

Paso 3: Ejecuta una prueba instrumentada

Las pruebas instrumentadas se encuentran en el conjunto de orígenes androidTest.

  1. Abre el conjunto de fuentes androidTest.
  2. Ejecuta la prueba llamada ExampleInstrumentedTest.

ExampleInstrumentedTest

@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
    @Test
    fun useAppContext() {
        // Context of the app under test.
        val appContext = InstrumentationRegistry.getInstrumentation().targetContext
        assertEquals("com.example.android.architecture.blueprints.reactive",
            appContext.packageName)
    }
}

A diferencia de la prueba local, esta prueba se ejecuta en un dispositivo (en el siguiente ejemplo, un teléfono Pixel 2 emulado):

Si tienes un dispositivo conectado o un emulador en ejecución, deberías ver la prueba ejecutándose en el emulador.

En esta tarea, escribirás pruebas para getActiveAndCompleteStats, que calcula el porcentaje de estadísticas de tareas activas y completadas para tu app. Puedes ver estos números en la pantalla de estadísticas de la app.

Paso 1: Crea una clase de prueba

  1. En el conjunto de fuentes main, en todoapp.statistics, abre StatisticsUtils.kt.
  2. Busca la función getActiveAndCompletedStats.

StatisticsUtils.kt

internal fun getActiveAndCompletedStats(tasks: List<Task>?): StatsResult {

   val totalTasks = tasks!!.size
   val numberOfActiveTasks = tasks.count { it.isActive }
   val activePercent = 100 * numberOfActiveTasks / totalTasks
   val completePercent = 100 * (totalTasks - numberOfActiveTasks) / totalTasks

   return StatsResult(
       activeTasksPercent = activePercent.toFloat(),
       completedTasksPercent = completePercent.toFloat()
   )
  
}

data class StatsResult(val activeTasksPercent: Float, val completedTasksPercent: Float)

La función getActiveAndCompletedStats acepta una lista de tareas y devuelve un StatsResult. StatsResult es una clase de datos que contiene dos números: el porcentaje de tareas completadas y el porcentaje de tareas activas.

Android Studio te proporciona herramientas para generar stubs de prueba que te ayudan a implementar las pruebas para esta función.

  1. Haz clic con el botón derecho en getActiveAndCompletedStats y selecciona Generate > Test.

Se abrirá el diálogo Crear prueba:

  1. Cambia el Nombre de la clase: a StatisticsUtilsTest (en lugar de StatisticsUtilsKtTest; es un poco mejor no tener KT en el nombre de la clase de prueba).
  2. Mantén el resto de los valores predeterminados. JUnit 4 es la biblioteca de pruebas adecuada. El paquete de destino es correcto (refleja la ubicación de la clase StatisticsUtils) y no necesitas marcar ninguna de las casillas de verificación (esto solo genera código adicional, pero escribirás tu prueba desde cero).
  3. Presiona OK.

Se abrirá el diálogo Choose Destination Directory:

Realizarás una prueba local porque tu función realiza cálculos matemáticos y no incluirá ningún código específico de Android. Por lo tanto, no es necesario ejecutarla en un dispositivo real o emulado.

  1. Selecciona el directorio test (no androidTest) porque escribirás pruebas locales.
  2. Haz clic en Aceptar.
  3. Observa la clase StatisticsUtilsTest generada en test/statistics/.

Paso 2: Escribe tu primera función de prueba

Escribirás una prueba que verifique lo siguiente:

  • Si no hay tareas completadas y hay una tarea activa
  • que el porcentaje de pruebas activas sea del 100%
  • y el porcentaje de tareas completadas es del 0%.
  1. Abre StatisticsUtilsTest.
  2. Crea una función llamada getActiveAndCompletedStats_noCompleted_returnsHundredZero.

StatisticsUtilsTest.kt

class StatisticsUtilsTest {

    fun getActiveAndCompletedStats_noCompleted_returnsHundredZero() {
        // Create an active task

        // Call your function

        // Check the result
    }
}
  1. Agrega la anotación @Test sobre el nombre de la función para indicar que es una prueba.
  2. Crea una lista de tareas.
// Create an active task 
val tasks = listOf<Task>(
            Task("title", "desc", isCompleted = false)
        )
  1. Llama a getActiveAndCompletedStats con estas tareas.
// Call your function
val result = getActiveAndCompletedStats(tasks)
  1. Verifica que result sea lo que esperabas con aserciones.
// Check the result
assertEquals(result.completedTasksPercent, 0f)
assertEquals(result.activeTasksPercent, 100f)

Este es el código completo.

StatisticsUtilsTest.kt

class StatisticsUtilsTest {

    @Test
    fun getActiveAndCompletedStats_noCompleted_returnsHundredZero() {

        // Create an active task (the false makes this active)
        val tasks = listOf<Task>(
            Task("title", "desc", isCompleted = false)
        )
        // Call your function
        val result = getActiveAndCompletedStats(tasks)

        // Check the result
        assertEquals(result.completedTasksPercent, 0f)
        assertEquals(result.activeTasksPercent, 100f)
    }
}
  1. Ejecuta la prueba (haz clic con el botón derecho en StatisticsUtilsTest y selecciona Ejecutar).

Debería pasar:

Paso 3: Agrega la dependencia de Hamcrest

Dado que las pruebas actúan como documentación de lo que hace tu código, es bueno que sean legibles para los humanos. Compara las siguientes dos aserciones:

assertEquals(result.completedTasksPercent, 0f)

// versus

assertThat(result.completedTasksPercent, `is`(0f))

La segunda afirmación se lee mucho más como una oración humana. Se escribe con un framework de aserción llamado Hamcrest. Otra buena herramienta para escribir aserciones legibles es la biblioteca de Truth. Usarás Hamcrest en este codelab para escribir aserciones.

  1. Abre build.grade (Module: app) y agrega la siguiente dependencia.

app/build.gradle

dependencies {
    // Other dependencies
    testImplementation "org.hamcrest:hamcrest-all:$hamcrestVersion"
}

Por lo general, usas implementation cuando agregas una dependencia, pero aquí usas testImplementation. Cuando tengas todo listo para compartir tu app con el mundo, es mejor no aumentar el tamaño del APK con ningún código de prueba ni dependencia de tu app. Puedes designar si una biblioteca debe incluirse en el código principal o de prueba con configuraciones de Gradle. Las configuraciones más comunes son las siguientes:

  • implementation: La dependencia está disponible en todos los conjuntos de orígenes, incluidos los conjuntos de orígenes de prueba.
  • testImplementation: La dependencia solo está disponible en el conjunto de fuentes de prueba.
  • androidTestImplementation: La dependencia solo está disponible en el conjunto de fuentes androidTest.

La configuración que uses definirá dónde se puede usar la dependencia. Si escribes lo siguiente:

testImplementation "org.hamcrest:hamcrest-all:$hamcrestVersion"

Esto significa que Hamcrest solo estará disponible en el conjunto de orígenes de prueba. También garantiza que Hamcrest no se incluirá en tu app final.

Paso 4: Usa Hamcrest para escribir aserciones

  1. Actualiza la prueba getActiveAndCompletedStats_noCompleted_returnsHundredZero() para usar assertThat de Hamcrest en lugar de assertEquals.
// REPLACE
assertEquals(result.completedTasksPercent, 0f)
assertEquals(result.activeTasksPercent, 100f)

// WITH
assertThat(result.activeTasksPercent, `is`(100f))
assertThat(result.completedTasksPercent, `is`(0f))

Ten en cuenta que puedes usar la importación import org.hamcrest.Matchers.`is` si se te solicita.

La prueba final se verá como el siguiente código.

StatisticsUtilsTest.kt

import com.example.android.architecture.blueprints.todoapp.data.Task
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.`is`
import org.junit.Test

class StatisticsUtilsTest {

    @Test
    fun getActiveAndCompletedStats_noCompleted_returnsHundredZero {

        // Create an active tasks (the false makes this active)
        val tasks = listOf<Task>(
            Task("title", "desc", isCompleted = false)
        )
        // Call your function
        val result = getActiveAndCompletedStats(tasks)

        // Check the result
        assertThat(result.activeTasksPercent, `is`(100f))
        assertThat(result.completedTasksPercent, `is`(0f))

    }
}
  1. Ejecuta la prueba actualizada para confirmar que siga funcionando.

En este codelab no aprenderás todos los detalles de Hamcrest, por lo que, si quieres obtener más información, consulta el tutorial oficial.

Esta es una tarea opcional para practicar.

En esta tarea, escribirás más pruebas con JUnit y Hamcrest. También escribirás pruebas con una estrategia derivada de la práctica de programación de Desarrollo basado en pruebas. El desarrollo basado en pruebas o TDD es una escuela de pensamiento de programación que dice que, en lugar de escribir primero el código de tu función, escribas primero tus pruebas. Luego, escribes el código de tu función con el objetivo de aprobar las pruebas.

Paso 1: Escribe las pruebas

Escribe pruebas para cuando tengas una lista de tareas normal:

  1. Si hay una tarea completada y ninguna tarea activa, el porcentaje de activeTasks debe ser 0f y el porcentaje de tareas completadas debe ser 100f .
  2. Si hay dos tareas completadas y tres tareas activas, el porcentaje de completadas debe ser 40f y el porcentaje de activas debe ser 60f.

Paso 2: Escribe una prueba para un error

El código del getActiveAndCompletedStats tal como está escrito tiene un error. Observa cómo no controla correctamente lo que sucede si la lista está vacía o es nula. En ambos casos, los porcentajes deben ser cero.

internal fun getActiveAndCompletedStats(tasks: List<Task>?): StatsResult {

   val totalTasks = tasks!!.size
   val numberOfActiveTasks = tasks.count { it.isActive }
   val activePercent = 100 * numberOfActiveTasks / totalTasks
   val completePercent = 100 * (totalTasks - numberOfActiveTasks) / totalTasks

   return StatsResult(
       activeTasksPercent = activePercent.toFloat(),
       completedTasksPercent = completePercent.toFloat()
   )
  
}

Para corregir el código y escribir pruebas, usarás el desarrollo basado en pruebas. El desarrollo basado en pruebas sigue estos pasos.

  1. Escribe la prueba con la estructura Given, When, Then y con un nombre que siga la convención.
  2. Confirma que la prueba falla.
  3. Escribe el código mínimo para que la prueba se apruebe.
  4. Repite este proceso para todas las pruebas.

En lugar de comenzar por corregir el error, primero escribirás las pruebas. Luego, puedes confirmar que tienes pruebas que te protegen de volver a introducir accidentalmente estos errores en el futuro.

  1. Si hay una lista vacía (emptyList()), ambos porcentajes deben ser 0f.
  2. Si se produjo un error al cargar las tareas, la lista será null y ambos porcentajes deberían ser 0f.
  3. Ejecuta las pruebas y confirma que fallan:

Paso 3: Corrige el error.

Ahora que tienes las pruebas, corrige el error.

  1. Corrige el error en getActiveAndCompletedStats devolviendo 0f si tasks es null o está vacío:
internal fun getActiveAndCompletedStats(tasks: List<Task>?): StatsResult {

    return if (tasks == null || tasks.isEmpty()) {
        StatsResult(0f, 0f)
    } else {
        val totalTasks = tasks.size
        val numberOfActiveTasks = tasks.count { it.isActive }
        StatsResult(
            activeTasksPercent = 100f * numberOfActiveTasks / tasks.size,
            completedTasksPercent = 100f * (totalTasks - numberOfActiveTasks) / tasks.size
        )
    }
}
  1. Vuelve a ejecutar las pruebas y confirma que ahora todas se aprueban.

Si sigues el TDD y escribes las pruebas primero, te aseguras de lo siguiente:

  • La nueva funcionalidad siempre tiene pruebas asociadas, por lo que las pruebas actúan como documentación de lo que hace tu código.
  • Tus pruebas verifican los resultados correctos y protegen contra los errores que ya viste.

Solución: Escribe más pruebas

A continuación, se indican todas las pruebas y el código de la función correspondiente.

StatisticsUtilsTest.kt

class StatisticsUtilsTest {

    @Test
    fun getActiveAndCompletedStats_noCompleted_returnsHundredZero {
        val tasks = listOf(
            Task("title", "desc", isCompleted = false)
        )
        // When the list of tasks is computed with an active task
        val result = getActiveAndCompletedStats(tasks)

        // Then the percentages are 100 and 0
        assertThat(result.activeTasksPercent, `is`(100f))
        assertThat(result.completedTasksPercent, `is`(0f))
    }

    @Test
    fun getActiveAndCompletedStats_noActive_returnsZeroHundred() {
        val tasks = listOf(
            Task("title", "desc", isCompleted = true)
        )
        // When the list of tasks is computed with a completed task
        val result = getActiveAndCompletedStats(tasks)

        // Then the percentages are 0 and 100
        assertThat(result.activeTasksPercent, `is`(0f))
        assertThat(result.completedTasksPercent, `is`(100f))
    }

    @Test
    fun getActiveAndCompletedStats_both_returnsFortySixty() {
        // Given 3 completed tasks and 2 active tasks
        val tasks = listOf(
            Task("title", "desc", isCompleted = true),
            Task("title", "desc", isCompleted = true),
            Task("title", "desc", isCompleted = true),
            Task("title", "desc", isCompleted = false),
            Task("title", "desc", isCompleted = false)
        )
        // When the list of tasks is computed
        val result = getActiveAndCompletedStats(tasks)

        // Then the result is 40-60
        assertThat(result.activeTasksPercent, `is`(40f))
        assertThat(result.completedTasksPercent, `is`(60f))
    }

    @Test
    fun getActiveAndCompletedStats_error_returnsZeros() {
        // When there's an error loading stats
        val result = getActiveAndCompletedStats(null)

        // Both active and completed tasks are 0
        assertThat(result.activeTasksPercent, `is`(0f))
        assertThat(result.completedTasksPercent, `is`(0f))
    }

    @Test
    fun getActiveAndCompletedStats_empty_returnsZeros() {
        // When there are no tasks
        val result = getActiveAndCompletedStats(emptyList())

        // Both active and completed tasks are 0
        assertThat(result.activeTasksPercent, `is`(0f))
        assertThat(result.completedTasksPercent, `is`(0f))
    }
}

StatisticsUtils.kt

internal fun getActiveAndCompletedStats(tasks: List<Task>?): StatsResult {

    return if (tasks == null || tasks.isEmpty()) {
        StatsResult(0f, 0f)
    } else {
        val totalTasks = tasks.size
        val numberOfActiveTasks = tasks.count { it.isActive }
        StatsResult(
            activeTasksPercent = 100f * numberOfActiveTasks / tasks.size,
            completedTasksPercent = 100f * (totalTasks - numberOfActiveTasks) / tasks.size
        )
    }
}

¡Excelente trabajo con los conceptos básicos para escribir y ejecutar pruebas! A continuación, aprenderás a escribir pruebas básicas de ViewModel y LiveData.

En el resto del codelab, aprenderás a escribir pruebas para dos clases de Android que son comunes en la mayoría de las apps: ViewModel y LiveData.

Comienza por escribir pruebas para TasksViewModel.


Te enfocarás en las pruebas que tienen toda su lógica en el modelo de vistas y no dependen del código del repositorio. El código del repositorio incluye código asíncrono, bases de datos y llamadas de red, lo que aumenta la complejidad de las pruebas. Por ahora, evitarás eso y te enfocarás en escribir pruebas para la funcionalidad de ViewModel que no pruebe directamente nada en el repositorio.



La prueba que escribirás verificará que, cuando llames al método addNewTask, se active el Event para abrir la ventana de la tarea nueva. Este es el código de la app que probarás.

TasksViewModel.kt

fun addNewTask() {
   _newTaskEvent.value = Event(Unit)
}

Paso 1: Crea una clase TasksViewModelTest

Siguiendo los mismos pasos que realizaste para StatisticsUtilTest, en este paso, crearás un archivo de prueba para TasksViewModelTest.

  1. Abre la clase que deseas probar en el paquete tasks. TasksViewModel.
  2. En el código, haz clic con el botón derecho en el nombre de la clase TasksViewModel -> Generate -> Test.

  1. En la pantalla Create Test, haz clic en OK para aceptar (no es necesario cambiar ninguno de los parámetros de configuración predeterminados).
  2. En el diálogo Choose Destination Directory, elige el directorio test.

Paso 2: Comienza a escribir tu prueba de ViewModel

En este paso, agregarás una prueba del modelo de vista para verificar que, cuando llames al método addNewTask, se active el Event para abrir la ventana de la tarea nueva.

  1. Crea una prueba nueva llamada addNewTask_setsNewTaskEvent.

TasksViewModelTest.kt

class TasksViewModelTest {

    @Test
    fun addNewTask_setsNewTaskEvent() {

        // Given a fresh TasksViewModel


        // When adding a new task


        // Then the new task event is triggered

    }
    
}

¿Qué sucede con el contexto de la aplicación?

Cuando creas una instancia de TasksViewModel para probarla, su constructor requiere un contexto de la aplicación. Sin embargo, en esta prueba, no crearás una aplicación completa con actividades, IU y fragmentos. Entonces, ¿cómo obtienes un contexto de aplicación?

TasksViewModelTest.kt

// Given a fresh ViewModel
        val tasksViewModel = TasksViewModel(???)

Las bibliotecas de prueba de AndroidX incluyen clases y métodos que te proporcionan versiones de componentes como Applications y Activities que están diseñados para pruebas. Cuando tengas una prueba local en la que necesites clases simuladas del framework de Android(como un contexto de aplicación), sigue estos pasos para configurar correctamente AndroidX Test:

  1. Agrega las dependencias principales y externas de AndroidX Test
  2. Agrega la dependencia de la biblioteca de pruebas de Robolectric
  3. Anota la clase con el ejecutor de pruebas AndroidJunit4
  4. Escribe código de AndroidX Test

Completarás estos pasos y luego comprenderás lo que hacen en conjunto.

Paso 3: Agrega las dependencias de Gradle

  1. Copia estas dependencias en el archivo build.gradle del módulo de tu app para agregar las dependencias principales de AndroidX Test y ext, así como la dependencia de prueba de Robolectric.

app/build.gradle

    // AndroidX Test - JVM testing
testImplementation "androidx.test.ext:junit-ktx:$androidXTestExtKotlinRunnerVersion"

    testImplementation "androidx.test:core-ktx:$androidXTestCoreVersion"

 testImplementation "org.robolectric:robolectric:$robolectricVersion"

Paso 4: Agrega el ejecutor de pruebas JUnit

  1. Agrega @RunWith(AndroidJUnit4::class)sobre tu clase de prueba.

TasksViewModelTest.kt

@RunWith(AndroidJUnit4::class)
class TasksViewModelTest {
    // Test code
}

Paso 5: Cómo usar AndroidX Test

En este punto, puedes usar la biblioteca de AndroidX Test. Esto incluye el método ApplicationProvider.getApplicationContext, que obtiene un contexto de aplicación.

  1. Crea un TasksViewModel con ApplicationProvider.getApplicationContext()de la biblioteca de prueba de AndroidX.

TasksViewModelTest.kt

// Given a fresh ViewModel
val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
  1. Llama a addNewTask en tasksViewModel.

TasksViewModelTest.kt

tasksViewModel.addNewTask()

En este punto, tu prueba debería verse como el siguiente código.

TasksViewModelTest.kt

    @Test
    fun addNewTask_setsNewTaskEvent() {

        // Given a fresh ViewModel
        val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())

        // When adding a new task
        tasksViewModel.addNewTask()

        // Then the new task event is triggered
        // TODO test LiveData
    }
  1. Ejecuta la prueba para confirmar que funcione.

Concepto: ¿Cómo funciona AndroidX Test?

¿Qué es AndroidX Test?

AndroidX Test es una colección de bibliotecas para pruebas. Incluye clases y métodos que te brindan versiones de componentes como Applications y Activities, que están diseñados para pruebas. Como ejemplo, este código que escribiste es un ejemplo de una función de AndroidX Test para obtener un contexto de aplicación.

ApplicationProvider.getApplicationContext()

Uno de los beneficios de las APIs de AndroidX Test es que están diseñadas para funcionar tanto en pruebas locales como en pruebas instrumentadas. Esto es útil por los siguientes motivos:

  • Puedes ejecutar la misma prueba como una prueba local o una prueba de instrumentación.
  • No necesitas aprender diferentes APIs de prueba para las pruebas locales y las instrumentadas.

Por ejemplo, como escribiste tu código con las bibliotecas de AndroidX Test, puedes mover tu clase TasksViewModelTest de la carpeta test a la carpeta androidTest y las pruebas seguirán ejecutándose. getApplicationContext() funciona de manera ligeramente diferente según si se ejecuta como una prueba local o instrumentada:

  • Si es una prueba instrumentada, obtendrá el contexto de Application real que se proporciona cuando se inicia un emulador o se conecta a un dispositivo real.
  • Si es una prueba local, se usa un entorno de Android simulado.

¿Qué es Robolectric?

Robolectric proporciona el entorno de Android simulado que AndroidX Test usa para las pruebas locales. Robolectric es una biblioteca que crea un entorno de Android simulado para las pruebas y se ejecuta más rápido que iniciar un emulador o ejecutar en un dispositivo. Sin la dependencia de Robolectric, recibirás este error:

¿Qué hace @RunWith(AndroidJUnit4::class)?

Un ejecutor de pruebas es un componente de JUnit que ejecuta pruebas. Sin un ejecutor de pruebas, estas no se ejecutarían. JUnit proporciona un ejecutor de pruebas predeterminado que obtienes automáticamente. @RunWith reemplaza ese panel de prueba predeterminado.

El panel de pruebas AndroidJUnit4 permite que AndroidX Test ejecute tu prueba de manera diferente según si se trata de pruebas instrumentadas o locales.

Paso 6: Cómo corregir las advertencias de Robolectric

Cuando ejecutes el código, observa que se usa Robolectric.

Gracias a AndroidX Test y al ejecutor de pruebas AndroidJunit4, esto se hace sin que escribas directamente una sola línea de código de Robolectric.

Es posible que veas dos advertencias.

  • No such manifest file: ./AndroidManifest.xml
  • "WARN: Android SDK 29 requires Java 9..."

Para corregir la advertencia No such manifest file: ./AndroidManifest.xml, actualiza tu archivo de Gradle.

  1. Agrega la siguiente línea a tu archivo Gradle para que se use el manifiesto de Android correcto. La opción includeAndroidResources te permite acceder a los recursos de Android en tus pruebas de unidades, incluido el archivo AndroidManifest.

app/build.gradle

    // Always show the result of every unit test when running via command line, even if it passes.
    testOptions.unitTests {
        includeAndroidResources = true

        // ... 
    }

La advertencia "WARN: Android SDK 29 requires Java 9..." es más complicada. Para ejecutar pruebas en Android Q, se requiere Java 9. En lugar de intentar configurar Android Studio para que use Java 9, en este codelab, mantén el SDK de destino y de compilación en 28.

Resumen:

  • Por lo general, las pruebas de modelos de vistas puros pueden ir en el conjunto de fuentes test porque su código no suele requerir Android.
  • Puedes usar la bibliotecade pruebas de AndroidX para obtener versiones de prueba de componentes como Applications y Activities.
  • Si necesitas ejecutar código de Android simulado en tu conjunto de fuentes test, puedes agregar la dependencia de Robolectric y la anotación @RunWith(AndroidJUnit4::class).

Felicitaciones, estás usando la biblioteca de pruebas de AndroidX y Robolectric para ejecutar una prueba. Tu prueba no está terminada (aún no escribiste una instrucción assert, solo dice // TODO test LiveData). Aprenderás a escribir instrucciones assert con LiveData a continuación.

En esta tarea, aprenderás a confirmar correctamente el valor de LiveData.

Aquí es donde quedaste sin la prueba del modelo de vista de addNewTask_setsNewTaskEvent.

TasksViewModelTest.kt

    @Test
    fun addNewTask_setsNewTaskEvent() {

        // Given a fresh ViewModel
        val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())

        // When adding a new task
        tasksViewModel.addNewTask()

        // Then the new task event is triggered
        // TODO test LiveData
    }
    

Para probar LiveData, te recomendamos que hagas dos cosas:

  1. Usa InstantTaskExecutorRule
  2. Garantizar la observación de LiveData

Paso 1: Usa InstantTaskExecutorRule

InstantTaskExecutorRule es una regla de JUnit. Cuando lo usas con la anotación @get:Rule, hace que se ejecute algo de código en la clase InstantTaskExecutorRule antes y después de las pruebas (para ver el código exacto, puedes usar la combinación de teclas Comando + B para ver el archivo).

Esta regla ejecuta todos los trabajos en segundo plano relacionados con los componentes de arquitectura en el mismo subproceso para que los resultados de las pruebas se produzcan de forma síncrona y en un orden repetible. Cuando escribas pruebas que incluyan pruebas de LiveData, usa esta regla.

  1. Agrega la dependencia de Gradle para la biblioteca principal de pruebas de Architecture Components (que contiene esta regla).

app/build.gradle

testImplementation "androidx.arch.core:core-testing:$archTestingVersion"
  1. Abrir TasksViewModelTest.kt
  2. Agrega InstantTaskExecutorRule dentro de la clase TasksViewModelTest.

TasksViewModelTest.kt

class TasksViewModelTest {
    @get:Rule
    var instantExecutorRule = InstantTaskExecutorRule()
    
    // Other code...
}

Paso 2: Cómo agregar la clase LiveDataTestUtil.kt

El siguiente paso es asegurarte de que se observe el LiveData que estás probando.

Cuando usas LiveData, es común que una actividad o un fragmento (LifecycleOwner) observen el LiveData.

viewModel.resultLiveData.observe(fragment, Observer {
    // Observer code here
})

Esta observación es importante. Necesitas observadores activos en LiveData para

Para obtener el comportamiento esperado de LiveData de tu modelo de vista, debes observar el LiveData con un LifecycleOwner.LiveData

Esto plantea un problema: en tu prueba de TasksViewModel, no tienes una actividad o un fragmento para observar tu LiveData. Para evitar esto, puedes usar el método observeForever, que garantiza que se observe el LiveData de forma constante, sin necesidad de un LifecycleOwner. Cuando usas observeForever, debes recordar quitar el observador o correrás el riesgo de que se produzca una pérdida de observador.

Se verá similar al siguiente código. Examínalo:

@Test
fun addNewTask_setsNewTaskEvent() {

    // Given a fresh ViewModel
    val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())


    // Create observer - no need for it to do anything!
    val observer = Observer<Event<Unit>> {}
    try {

        // Observe the LiveData forever
        tasksViewModel.newTaskEvent.observeForever(observer)

        // When adding a new task
        tasksViewModel.addNewTask()

        // Then the new task event is triggered
        val value = tasksViewModel.newTaskEvent.value
        assertThat(value?.getContentIfNotHandled(), (not(nullValue())))

    } finally {
        // Whatever happens, don't forget to remove the observer!
        tasksViewModel.newTaskEvent.removeObserver(observer)
    }
}

Es mucho código estándar para observar un solo LiveData en una prueba. Existen varias formas de deshacerte de este código boilerplate. Crearás una función de extensión llamada LiveDataTestUtil para simplificar la adición de observadores.

  1. Crea un nuevo archivo Kotlin llamado LiveDataTestUtil.kt en tu conjunto de fuentes test.


  1. Copia y pega el siguiente código.

LiveDataTestUtil.kt

import androidx.annotation.VisibleForTesting
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException


@VisibleForTesting(otherwise = VisibleForTesting.NONE)
fun <T> LiveData<T>.getOrAwaitValue(
    time: Long = 2,
    timeUnit: TimeUnit = TimeUnit.SECONDS,
    afterObserve: () -> Unit = {}
): T {
    var data: T? = null
    val latch = CountDownLatch(1)
    val observer = object : Observer<T> {
        override fun onChanged(o: T?) {
            data = o
            latch.countDown()
            this@getOrAwaitValue.removeObserver(this)
        }
    }
    this.observeForever(observer)

    try {
        afterObserve.invoke()

        // Don't wait indefinitely if the LiveData is not set.
        if (!latch.await(time, timeUnit)) {
            throw TimeoutException("LiveData value was never set.")
        }

    } finally {
        this.removeObserver(observer)
    }

    @Suppress("UNCHECKED_CAST")
    return data as T
}

Este es un método bastante complicado. Crea una función de extensión de Kotlin llamada getOrAwaitValue que agrega un observador, obtiene el valor de LiveData y, luego, limpia el observador, básicamente, una versión corta y reutilizable del código observeForever que se muestra arriba. Para obtener una explicación completa de esta clase, consulta esta entrada de blog.

Paso 3: Usa getOrAwaitValue para escribir la aserción

En este paso, usarás el método getOrAwaitValue y escribirás una sentencia assert que verifique que se haya activado el newTaskEvent.

  1. Obtén el valor de LiveData para newTaskEvent con getOrAwaitValue.
val value = tasksViewModel.newTaskEvent.getOrAwaitValue()
  1. Afirma que el valor no es nulo.
assertThat(value.getContentIfNotHandled(), (not(nullValue())))

La prueba completa debería verse como el siguiente código.

import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.example.android.architecture.blueprints.todoapp.getOrAwaitValue
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.not
import org.hamcrest.Matchers.nullValue
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class TasksViewModelTest {

    @get:Rule
    var instantExecutorRule = InstantTaskExecutorRule()


    @Test
    fun addNewTask_setsNewTaskEvent() {
        // Given a fresh ViewModel
        val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())

        // When adding a new task
        tasksViewModel.addNewTask()

        // Then the new task event is triggered
        val value = tasksViewModel.newTaskEvent.getOrAwaitValue()

        assertThat(value.getContentIfNotHandled(), not(nullValue()))


    }

}
  1. Ejecuta tu código y observa cómo se aprueba la prueba.

Ahora que viste cómo escribir una prueba, escribe una por tu cuenta. En este paso, con las habilidades que adquiriste, practica la escritura de otra prueba de TasksViewModel.

Paso 1: Escribe tu propia prueba de ViewModel

Escribirás setFilterAllTasks_tasksAddViewVisible(). Esta prueba debe verificar que, si configuraste el tipo de filtro para mostrar todas las tareas, el botón Agregar tarea esté visible.

  1. Usando addNewTask_setsNewTaskEvent() como referencia, escribe una prueba en TasksViewModelTest llamada setFilterAllTasks_tasksAddViewVisible() que establezca el modo de filtrado en ALL_TASKS y confirme que el LiveData tasksAddViewVisible es true.


Usa el siguiente código para comenzar.

TasksViewModelTest

    @Test
    fun setFilterAllTasks_tasksAddViewVisible() {

        // Given a fresh ViewModel

        // When the filter type is ALL_TASKS

        // Then the "Add task" action is visible
        
    }

Nota:

  • El enum TasksFilterType para todas las tareas es ALL_TASKS..
  • La visibilidad del botón para agregar una tarea se controla con LiveData tasksAddViewVisible.
  1. Ejecuta la prueba.

Paso 2: Compara tu prueba con la solución

Compara tu solución con la que se muestra a continuación.

TasksViewModelTest

    @Test
    fun setFilterAllTasks_tasksAddViewVisible() {

        // Given a fresh ViewModel
        val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())

        // When the filter type is ALL_TASKS
        tasksViewModel.setFiltering(TasksFilterType.ALL_TASKS)

        // Then the "Add task" action is visible
        assertThat(tasksViewModel.tasksAddViewVisible.getOrAwaitValue(), `is`(true))
    }

Verifica si cumples con los siguientes requisitos:

  • Creas tu tasksViewModel con la misma instrucción ApplicationProvider.getApplicationContext() de AndroidX.
  • Llamas al método setFiltering y pasas el enum del tipo de filtro ALL_TASKS.
  • Verificas que tasksAddViewVisible sea verdadero con el método getOrAwaitNextValue.

Paso 3: Agrega una regla @Before

Observa cómo, al comienzo de ambas pruebas, defines un TasksViewModel.

TasksViewModelTest

        // Given a fresh ViewModel
        val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())

Cuando tienes código de configuración repetido para varias pruebas, puedes usar la anotación @Before para crear un método de configuración y quitar el código repetido. Dado que todas estas pruebas probarán el TasksViewModel y necesitan un modelo de vista, mueve este código a un bloque @Before.

  1. Crea una variable de instancia lateinit llamada tasksViewModel|.
  2. Crea un método llamado setupViewModel.
  3. Anótalo con @Before.
  4. Mueve el código de creación de instancias del modelo de vista a setupViewModel.

TasksViewModelTest

    // Subject under test
    private lateinit var tasksViewModel: TasksViewModel

    @Before
    fun setupViewModel() {
        tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
    }
  1. Ejecuta tu código.

Advertencia

No hagas lo siguiente, no inicialices el

tasksViewModel

con su definición:

val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())

Esto hará que se use la misma instancia para todas las pruebas. Debes evitar esto porque cada prueba debe tener una instancia nueva del sujeto de prueba (el ViewModel en este caso).

El código final de TasksViewModelTest debería verse como el siguiente.

TasksViewModelTest

@RunWith(AndroidJUnit4::class)
class TasksViewModelTest {

    // Subject under test
    private lateinit var tasksViewModel: TasksViewModel

    // Executes each task synchronously using Architecture Components.
    @get:Rule
    var instantExecutorRule = InstantTaskExecutorRule()

    @Before
    fun setupViewModel() {
        tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
    }


    @Test
    fun addNewTask_setsNewTaskEvent() {

        // When adding a new task
        tasksViewModel.addNewTask()

        // Then the new task event is triggered
        val value = tasksViewModel.newTaskEvent.awaitNextValue()
        assertThat(
            value?.getContentIfNotHandled(), (not(nullValue()))
        )
    }

    @Test
    fun getTasksAddViewVisible() {

        // When the filter type is ALL_TASKS
        tasksViewModel.setFiltering(TasksFilterType.ALL_TASKS)

        // Then the "Add task" action is visible
        assertThat(tasksViewModel.tasksAddViewVisible.awaitNextValue(), `is`(true))
    }
    
}

Haz clic aquí para ver una comparación entre el código con el que comenzaste y el código final.

Para descargar el código del codelab terminado, puedes usar el siguiente comando de git:

$ git clone https://github.com/googlecodelabs/android-testing.git
$ cd android-testing
$ git checkout end_codelab_1


También puedes descargar el repositorio como un archivo ZIP, descomprimirlo y abrirlo en Android Studio.

Download Zip

En este codelab, se abordaron los siguientes temas:

  • Cómo ejecutar pruebas desde Android Studio
  • La diferencia entre las pruebas locales (test) y las de instrumentación (androidTest)
  • Cómo escribir pruebas de unidades locales con JUnit y Hamcrest
  • Cómo configurar pruebas de ViewModel con la biblioteca de AndroidX Test

Curso de Udacity:

Documentación para desarrolladores de Android:

Videos:

Otro:

Para obtener vínculos a otros codelabs de este curso, consulta la página de destino de los codelabs de Aspectos avanzados de Android en Kotlin.