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:
- El lenguaje de programación Kotlin
- Las siguientes bibliotecas principales de Android Jetpack:
ViewModel
yLiveData
- Arquitectura de la aplicación, siguiendo el patrón de la Guía de arquitectura de apps y los codelabs de Android Fundamentals
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:
- JUnit4
- Hamcrest
- Biblioteca de prueba de AndroidX
- Biblioteca principal de pruebas de los componentes de arquitectura de AndroidX
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
yViewModel
.
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:
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:
- Codelab de Room con un componente View
- Codelabs de capacitación sobre los conceptos básicos de Kotlin para Android
- Codelabs de capacitación avanzada de Android
- Muestra de Android Sunflower
- Curso de capacitación de Udacity sobre el desarrollo de apps para Android con Kotlin
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: | |
| Pantalla para agregar o editar una tarea: Código de la capa de la IU para agregar o editar una tarea. |
| 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. |
| La pantalla de estadísticas: Código de la capa de la IU para la pantalla de estadísticas. |
| La pantalla de detalles de la tarea: Código de la capa de IU para una sola tarea. |
| La pantalla de tareas: Código de la capa de IU para la lista de todas las tareas. |
| 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.
- 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
- Abre la carpeta
test
hasta que encuentres el archivo ExampleUnitTest.kt. - 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:
- 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.
- Agrega
assertEquals(3, 1 + 1)
a la pruebaaddition_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
}
}
- Ejecuta la prueba.
- En los resultados de la prueba, observa una X junto a la prueba.
- 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
.
- Abre el conjunto de fuentes
androidTest
. - 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
- En el conjunto de fuentes
main
, entodoapp.statistics
, abreStatisticsUtils.kt
. - 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.
- Haz clic con el botón derecho en
getActiveAndCompletedStats
y selecciona Generate > Test.
Se abrirá el diálogo Crear prueba:
- Cambia el Nombre de la clase: a
StatisticsUtilsTest
(en lugar deStatisticsUtilsKtTest
; es un poco mejor no tener KT en el nombre de la clase de prueba). - 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). - 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.
- Selecciona el directorio
test
(noandroidTest
) porque escribirás pruebas locales. - Haz clic en Aceptar.
- Observa la clase
StatisticsUtilsTest
generada entest/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%.
- Abre
StatisticsUtilsTest
. - 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
}
}
- Agrega la anotación
@Test
sobre el nombre de la función para indicar que es una prueba. - Crea una lista de tareas.
// Create an active task
val tasks = listOf<Task>(
Task("title", "desc", isCompleted = false)
)
- Llama a
getActiveAndCompletedStats
con estas tareas.
// Call your function
val result = getActiveAndCompletedStats(tasks)
- 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)
}
}
- 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.
- 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 fuentesandroidTest
.
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
- Actualiza la prueba
getActiveAndCompletedStats_noCompleted_returnsHundredZero()
para usarassertThat
de Hamcrest en lugar deassertEquals
.
// 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))
}
}
- 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:
- Si hay una tarea completada y ninguna tarea activa, el porcentaje de
activeTasks
debe ser0f
y el porcentaje de tareas completadas debe ser100f
. - Si hay dos tareas completadas y tres tareas activas, el porcentaje de completadas debe ser
40f
y el porcentaje de activas debe ser60f
.
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.
- Escribe la prueba con la estructura Given, When, Then y con un nombre que siga la convención.
- Confirma que la prueba falla.
- Escribe el código mínimo para que la prueba se apruebe.
- 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.
- Si hay una lista vacía (
emptyList()
), ambos porcentajes deben ser 0f. - Si se produjo un error al cargar las tareas, la lista será
null
y ambos porcentajes deberían ser 0f. - Ejecuta las pruebas y confirma que fallan:
Paso 3: Corrige el error.
Ahora que tienes las pruebas, corrige el error.
- Corrige el error en
getActiveAndCompletedStats
devolviendo0f
sitasks
esnull
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
)
}
}
- 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
.
- Abre la clase que deseas probar en el paquete
tasks
.TasksViewModel.
- En el código, haz clic con el botón derecho en el nombre de la clase
TasksViewModel
-> Generate -> Test.
- En la pantalla Create Test, haz clic en OK para aceptar (no es necesario cambiar ninguno de los parámetros de configuración predeterminados).
- 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.
- 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:
- Agrega las dependencias principales y externas de AndroidX Test
- Agrega la dependencia de la biblioteca de pruebas de Robolectric
- Anota la clase con el ejecutor de pruebas AndroidJunit4
- 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
- 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
- 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.getApplicationContex
t
, que obtiene un contexto de aplicación.
- Crea un
TasksViewModel
conApplicationProvider.getApplicationContext()
de la biblioteca de prueba de AndroidX.
TasksViewModelTest.kt
// Given a fresh ViewModel
val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
- Llama a
addNewTask
entasksViewModel
.
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
}
- 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.
- 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:
- Usa
InstantTaskExecutorRule
- 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.
- 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"
- Abrir
TasksViewModelTest.kt
- Agrega
InstantTaskExecutorRule
dentro de la claseTasksViewModelTest
.
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
- activar cualquier evento
onChanged
- activar cualquier Transformación
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.
- Crea un nuevo archivo Kotlin llamado
LiveDataTestUtil.kt
en tu conjunto de fuentestest
.
- 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
.
- Obtén el valor de
LiveData
paranewTaskEvent
congetOrAwaitValue
.
val value = tasksViewModel.newTaskEvent.getOrAwaitValue()
- 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()))
}
}
- 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.
- Usando
addNewTask_setsNewTaskEvent()
como referencia, escribe una prueba enTasksViewModelTest
llamadasetFilterAllTasks_tasksAddViewVisible()
que establezca el modo de filtrado enALL_TASKS
y confirme que el LiveDatatasksAddViewVisible
estrue
.
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 esALL_TASKS.
. - La visibilidad del botón para agregar una tarea se controla con
LiveData
tasksAddViewVisible.
- 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ónApplicationProvider.getApplicationContext()
de AndroidX. - Llamas al método
setFiltering
y pasas el enum del tipo de filtroALL_TASKS
. - Verificas que
tasksAddViewVisible
sea verdadero con el métodogetOrAwaitNextValue
.
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
.
- Crea una variable de instancia
lateinit
llamadatasksViewModel|
. - Crea un método llamado
setupViewModel
. - Anótalo con
@Before
. - 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())
}
- 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.
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:
- Guía de arquitectura de apps
- JUnit4
- Hamcrest
- Biblioteca de pruebas de Robolectric
- Biblioteca de prueba de AndroidX
- Biblioteca principal de pruebas de los componentes de arquitectura de AndroidX
- conjuntos de fuentes
- Cómo realizar pruebas desde la línea de comandos
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.