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 en secuencia, pero no es obligatorio. Todos los codelabs del curso se detallan en la página de destino de Codelabs avanzados de Android en Kotlin.

Introducción

Cuando implementaste la primera función de la primera app, es probable que hayas ejecutado el código para verificar que funcionó como se esperaba. Realizaste una prueba, aunque una prueba manual. A medida que continuaste agregando y actualizando funciones, es probable que también siguieras ejecutando tu código y verificando que funciona. Pero hacerlo de forma manual siempre es cansador, propenso a errores y no se ajusta.

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 manualmente para verificar que funcione el código.

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

En este primer codelab, se abordan los aspectos básicos de las pruebas en Android. Deberás escribir tus primeras pruebas y aprender a realizar pruebas con 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

  • Configura, ejecuta e interpreta pruebas locales e instrumentadas en Android.
  • Escribe 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 te permite escribir tareas para completarlas y mostrarlas en una lista. Luego, puede marcarlos como completados o no, filtrarlos o borrarlos.

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 arquitecturas.

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 muestra

Una vez que hayas descargado la app Pendientes, ábrela en Android Studio y ejecútala. Debería compilarse. Para explorar la app, haz lo siguiente:

  • Crea una nueva tarea con el botón de acción flotante más. Ingresa un título y, luego, información adicional sobre la tarea. Guárdalo con el BAF de verificación verde.
  • En la lista de tareas, haz clic en el título de la tarea que acabas de completar y observa la pantalla de detalles de esa tarea para ver el resto de la descripción.
  • En la lista o en la pantalla de detalles, marque la casilla de verificación de esa tarea para establecer su estado en Completado.
  • Regrese a la pantalla de tareas, abra el menú de filtro y filtre las tareas por estado Activo y Completado.
  • 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 lateral de navegación, selecciona Borrar tareas para borrar todas las tareas que tengan el estado Completadas.

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

La app Pendientes se basa en las populares pruebas y arquitectura de Architecture Blueprints (que usan 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 fragmentos, un repositorio y Room. Si conoces alguno de los ejemplos que se incluyen a continuación, esta app tiene una arquitectura similar:

Es más importante que comprendas la arquitectura general de la app que una comprensión profunda de la lógica en una capa.

A continuación, se muestra el resumen de los paquetes que encontrarás:

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

.addedittask

Pantalla de agregar o editar una tarea: Es el código de la capa de la IU para agregar o editar una tarea.

.data

Capa de datos: Esta opción se aplica a la capa de datos de las tareas. Contiene la base de datos, la red y el código del repositorio.

.statistics

Pantalla de estadísticas: Es el código de la capa de la IU para la pantalla de estadísticas.

.taskdetail

Pantalla de detalles de la tarea: Es el código de la capa de la IU de una sola tarea.

.tasks

Pantalla de tareas: Es el código de la capa de la IU para la lista de todas las tareas.

.util

Clases de utilidad: Son clases compartidas que se usan en varias partes de la app, p.ej., para el diseño de actualización de 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 herramientas de redes se simula con solo un HashMap con un retraso, en lugar de realizar solicitudes de red reales.

DefaultTasksRepository coordina o media la capa de red y la de la base de datos, y es lo que muestra los datos en la capa de la IU.

Capa de la IU ( .addingittask, .statistics, .taskdetail y .tasks)

Cada uno de los paquetes de la capa de la 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). El elemento TaskActivity es la actividad que contiene todos los fragmentos.

Navegación

El componente de Navigation controla la navegación de la app. Se define en el archivo nav_graph.xml. Se activa la navegación 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 las siguientes 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 orígenes. Los conjuntos de orígenes son carpetas que contienen el código fuente de tu app. Los conjuntos de orígenes de color verde (androidTest y test) contienen las pruebas. Cuando creas un nuevo proyecto de Android, obtienes los siguientes tres conjuntos de orígenes de forma predeterminada. Son las siguientes:

  • main: contiene el código de la 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 instrumentadas.
  • test: Contiene pruebas conocidas como pruebas locales.

La diferencia entre las pruebas locales y las pruebas de instrumentación se basa en la forma de ejecutarlas.

Pruebas locales (conjunto de orígenestest)

Estas pruebas se ejecutan de forma local en tu JVM de la 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 de lo que 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 (androidTest conjunto de orígenes)

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

En Android Studio, las pruebas instrumentadas se representan con un ícono de Android con un ícono de 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 ella y selecciona Run ExampleUnitTest.

Debería 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 aprobó una prueba llamada addition_isCorrect. Es bueno saber que la adición funciona como se espera.

Paso 2: Haz que la prueba falle

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)
   }
}

Observa que las pruebas

  • son una clase en uno de los conjuntos de orígenes de pruebas.
  • contienen funciones que comienzan con la anotación @Test (cada función es una prueba única).
  • Contienen aserciones universales de aserción.

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

Una aserción es el núcleo de tu prueba. Es una declaración de código que verifica que el código o la app se comporten como se espera. 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 con errores, agrega una aserción que puedas ver fácilmente. Se verificará que 3 sea igual a 1+1.

  1. Agrega assertEquals(3, 1 + 1) a la prueba de 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 ella.

  1. También ten en cuenta lo siguiente:
  • Una sola aserción fallida falla toda la prueba.
  • Se le informa el valor esperado (3) en comparación con el valor que se calculó (2).
  • Se te redireccionará a la línea de la aserción con errores (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 orígenes androidTest.
  2. Ejecuta la prueba llamada ExampleInstrumentedTest.

EjemplodeInstrumentedTest

@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 se ejecuta en un dispositivo (en el siguiente ejemplo, se muestra un teléfono Pixel 2 emulado):

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

En esta tarea, escribirás pruebas para getActiveAndCompleteStats, que calculan el porcentaje de estadísticas de tareas activas y completas de 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 orígenes 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 muestra un StatsResult. StatsResult es una clase de datos que contiene dos números, el porcentaje de tareas completadas y el activo.

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

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

Se abrirá el diálogo Create Test:

  1. Cambia Class name: por StatisticsUtilsTest (en lugar de StatisticsUtilsKtTest; es un poco más agradable a no tener KT en el nombre de la clase de prueba).
  2. Conserva el resto de las opciones predeterminadas. JUnit 4 es la biblioteca de pruebas adecuada. El paquete de destino es correcto (duplica 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á código específico de Android. Por lo tanto, no es necesario ejecutarlo en un dispositivo real o emulado.

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

Paso 2: Escribe tu primera función de prueba

Vas a escribir una prueba que verifique lo siguiente:

  • si no hay tareas completadas y una tarea activa,
  • que el porcentaje de pruebas activas es 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. Comprueba que result sea lo que esperabas mediante 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 Run).

Debería aprobarse lo siguiente:

Paso 3: Agrega la dependencia de Hamcrest

Debido a que las pruebas actúan como documentación de lo que hace el código, es atractivo cuando es legible. Compara las siguientes dos aserciones:

assertEquals(result.completedTasksPercent, 0f)

// versus

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

La segunda aserción lee mucho más como una oración humana. Se escribe con un marco de trabajo de aserción llamado Hamcrest. Otra buena herramienta para escribir aserciones legibles es la biblioteca de Truth. En este codelab, usarás Hamcrest 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 usas testImplementation. Cuando estés listo para compartir tu app con el mundo, te recomendamos que no aumentes el tamaño de tu APK con el código o las dependencias de la app. Puedes indicar si se debe incluir una biblioteca en el código principal o de prueba mediante 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 de prueba.
  • testImplementation: La dependencia solo está disponible en el conjunto de orígenes de prueba.
  • androidTestImplementation: La dependencia solo está disponible en el conjunto de orígenes androidTest.

La configuración que usas define dónde se puede usar la dependencia. Si escribes:

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 incluya en tu app final.

Paso 4: Usa Hamcrest para escribir aserciones

  1. Actualiza la prueba de 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 import org.hamcrest.Matchers.`is` de importación 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.

Este codelab no te enseñará todos los detalles de Hamcrest, por lo que, si quieres obtener más información, consulta el instructivo 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 del programa de desarrollo basado en pruebas. El Desarrollo controlado por pruebas (TDD, por sus siglas en inglés) es una escuela de programación que dice que, en lugar de escribir el código de funciones primero, escribes las pruebas. Luego, escribes el código de función con el objetivo de pasar las pruebas.

Paso 1: Cómo escribir las pruebas

Escribe pruebas para cuando tengas una lista normal de tareas:

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

Paso 2: Escribe una prueba para un error

El código para getActiveAndCompletedStats, como está escrito, tiene un error. Observa que no maneja adecuadamente lo que ocurre si la lista está vacía o es nula. En ambos casos, ambos 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. Escriba la prueba con la estructura dada, cuándo, luego 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 pase.
  4. Repite el procedimiento para todas las pruebas.

En lugar de comenzar por corregir el error, primero deberás escribir las pruebas. Luego, podrás confirmar si hay pruebas que te protegen de futuros errores.

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

Paso 3: Corrige el error

Ahora que tienes las pruebas, corrige el error.

  1. Para corregir el error en getActiveAndCompletedStats, muestra 0f si tasks está 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 ya se aprobaron todas.

Sigues el TDD y escribes primero las pruebas para ayudarnos a garantizar lo siguiente:

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

Solución: Cómo escribir más pruebas

Estas son todas las pruebas y el código de 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
        )
    }
}

Felicitaciones por los conceptos básicos de escritura y ejecución de pruebas. A continuación, aprenderás a escribir pruebas básicas 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.

Comienzas escribiendo pruebas para TasksViewModel.


Te enfocarás en pruebas que tienen toda su lógica en el modelo de vista 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 prueba directamente nada en el repositorio.



La prueba que escribas verificará que, cuando llames al método addNewTask, se active el elemento Event para abrir la ventana de tareas 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

Sigue los mismos pasos que para StatisticsUtilTest. En este paso, crea 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 la configuración predeterminada).
  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 de modelos de vistas para probar que, cuando llames al método addNewTask, se active el elemento Event para abrir la ventana de tareas 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 la prueba, su constructor requiere un contexto de aplicación. Sin embargo, en esta prueba, no crearás una aplicación completa con actividades, IU y fragmentos, de manera que ¿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 aplicaciones y actividades, que están diseñadas para pruebas. Cuando tengas una prueba local en la que necesites clases del framework de Android simuladas (como un contexto de aplicación), sigue estos pasos para configurar correctamente la prueba de AndroidX:

  1. Cómo agregar las dependencias principales y externas de AndroidX Test
  2. Agrega la dependencia Robolectric Testing
  3. Anotar la clase con el ejecutor de pruebas de AndroidJunit4
  4. Cómo escribir el código de prueba de AndroidX

Deberás completar estos pasos y, luego comprender lo que hacen juntos.

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 y centrales de AndroidX Test, además de la dependencia de prueba 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 un ejecutor de prueba de JUnit

  1. Agrega @RunWith(AndroidJUnit4::class) arriba de la 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 prueba de AndroidX. Esto incluye el método ApplicationProvider.getApplicationContext, que obtiene un contexto de aplicación.

  1. Crea un TasksViewModel usando 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 funciona.

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 aplicaciones y actividades, que están destinados a pruebas. Por ejemplo, este código que escribiste es un ejemplo de una función de prueba de AndroidX para obtener el contexto de una aplicación.

ApplicationProvider.getApplicationContext()

Uno de los beneficios de las API de AndroidX Test es que están diseñados para funcionar con pruebas locales y pruebas instrumentadas. Esto es bueno porque:

  • Puedes ejecutar la misma prueba que una local o una instrumentada.
  • No es necesario que aprendas diferentes API de prueba para pruebas locales frente a instrumentadas.

Por ejemplo, como escribiste tu código con las bibliotecas de prueba de AndroidX, puedes mover tu clase TasksViewModelTest de la carpeta test a la carpeta androidTest, y las pruebas se seguirán ejecutando. El funcionamiento de getApplicationContext() es ligeramente diferente en función de si se ejecuta como prueba local o instrumentada:

  • Si se trata de una prueba de instrumentación, obtendrá el contexto real de la aplicación proporcionado cuando se inicie un emulador o se conecte a un dispositivo real.
  • Si se trata de una prueba local, utiliza un entorno Android simulado.

¿Qué es Robolectric?

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

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

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

El ejecutor 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, ten en cuenta que se usa Robolectric.

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

Quizás 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 el archivo de Gradle.

  1. Agrega la siguiente línea a tu archivo de 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. La ejecución de pruebas en Android Q requiere Java 9. En lugar de intentar configurar Android Studio para que use Java 9, en este codelab, mantén tu destino y compila el SDK en 28.

Resumen:

  • Por lo general, las pruebas de modelo de vista pura pueden ir en el conjunto de orígenes test porque su código no suele requerir Android.
  • Puedes usar la biblioteca de pruebas de AndroidX para obtener versiones de prueba de componentes como aplicaciones y actividades.
  • Si necesitas ejecutar código de Android simulado en tu conjunto de orígenes 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 ha terminado (aún no escribiste una declaración afirmativa; solo dice // TODO test LiveData). A continuación, aprenderás a escribir sentencias con LiveData.

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

Quedaste aquí sin una prueba de 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, se recomienda que hagas dos cosas:

  1. Usa InstantTaskExecutorRule
  2. Asegúrate de observar LiveData

Paso 1: Usa InstantTaskExecutorRule

InstantTaskExecutorRule es una Regla de JUnit. Cuando lo usas con la anotación @get:Rule, se ejecuta parte del código de 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 a fin de ver el archivo).

Esta regla ejecuta todos los trabajos en segundo plano relacionados con los componentes de la arquitectura en el mismo subproceso a fin de que los resultados de la prueba se realicen de forma síncrona y en un orden recurrente. Cuando escribas pruebas que incluyan pruebas de LiveData, usa esta regla.

  1. Agrega la dependencia de Gradle para la biblioteca de prueba principal de componentes de la arquitectura (que contiene esta regla).

app/build.gradle

testImplementation "androidx.arch.core:core-testing:$archTestingVersion"
  1. Abrir TasksViewModelTest.kt
  2. Agrega el elemento 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 próximo paso es asegurarse de que se cumplan las LiveData pruebas.

Cuando usas LiveData, por lo general, tienes una actividad o un fragmento (LifecycleOwner) que observa LiveData.

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

Esta observación es importante. Necesitas observadores activos en LiveData para hacer lo siguiente:

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

Esto plantea un problema: en la prueba de TasksViewModel, no tienes una actividad ni un fragmento para observar tu LiveData. Para solucionar esto, puedes usar el método observeForever, que garantiza que el LiveData se observe de manera constante, sin necesidad de un LifecycleOwner. Cuando observeForever, debes recordar quitar a tu observador o correr el riesgo de que se produzca una fuga de observador.

Es similar al siguiente código. Explóralo:

@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)
    }
}

Se trata de mucho código estándar para observar un único elemento LiveData en una prueba. Existen varias maneras de deshacerse de este código estándar. Vas a crear 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 orígenes test.


  1. Copie y pegue 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 LiveData y, luego, borra el observador, que es 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 declaración de aserción que verifique que se activó newTaskEvent.

  1. Obtén el valor de LiveData para newTaskEvent mediante getOrAwaitValue.
val value = tasksViewModel.newTaskEvent.getOrAwaitValue()
  1. Confirma que el valor no sea 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 el código y observa la prueba.

Ahora que aprendiste a escribir una prueba, escribe una por tu cuenta. En este paso, usa las habilidades que aprendiste para practicar la escritura de otra prueba TasksViewModel.

Paso 1: Escribe tu propia prueba de ViewModel

Escribirás setFilterAllTasks_tasksAddViewVisible(). Esta prueba verifica si el botón Agregar tarea está visible si configuraste tu tipo de filtro para mostrar todas las tareas.

  1. Con addNewTask_setsNewTaskEvent() a modo de referencia, escribe una prueba en TasksViewModelTest llamada setFilterAllTasks_tasksAddViewVisible() que establezca el modo de filtrado en ALL_TASKS y confirme que el LiveData de tasksAddViewVisible sea 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:

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

Paso 2: Compare su prueba con la solución

Compara tu solución con la siguiente.

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))
    }

Comprueba si haces lo siguiente:

  • Puedes crear tu tasksViewModel con la misma sentencia ApplicationProvider.getApplicationContext() de AndroidX.
  • Llama al método setFiltering y pasa la enumeración de tipo de filtro ALL_TASKS.
  • Verifica que tasksAddViewVisible sea verdadero mediante el método getOrAwaitNextValue.

Paso 3: Cómo agregar una regla @Before

Observa que, al comienzo de ambas pruebas, defines un TasksViewModel.

TasksViewModelTest

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

Cuando hayas repetido el código de configuración para varias pruebas, puedes usar la anotación @Before a fin de crear un método de configuración y quitar el código repetido. Como todas estas pruebas probarán 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 haga lo siguiente, no inicialice 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, ya que cada prueba debe tener una instancia nueva del sujeto de prueba (en este caso, el ViewModel).

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 diferencia entre el código 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 trató lo siguiente:

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

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 Codelabs avanzados de Android en Kotlin.