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
Este segundo codelab de pruebas se centra en las pruebas dobles: cuándo usarlas en Android y cómo implementarlas con la inyección de dependencias, el patrón de Service Locator y las bibliotecas. De esta manera, aprenderás a escribir lo siguiente:
- Pruebas de unidades del repositorio
- Pruebas de integración de fragmentos y ViewModel
- Pruebas de navegación de fragmentos
Conocimientos que ya deberías tener
Debes estar familiarizado con lo siguiente:
- El lenguaje de programación Kotlin
- Conceptos de pruebas que se abordan en el primer codelab: Cómo escribir y ejecutar pruebas de unidades en Android, y cómo usar JUnit, Hamcrest, pruebas de AndroidX, Robolectric y pruebas de LiveData
- Las siguientes bibliotecas principales de Android Jetpack:
ViewModel,LiveDatay el componente de Navigation - Arquitectura de la aplicación, siguiendo el patrón de la Guía de arquitectura de apps y los codelabs de Android Fundamentals
- Conceptos básicos de las corrutinas en Android
Qué aprenderás
- Cómo planificar una estrategia de pruebas
- Cómo crear y usar dobles de prueba, es decir, simulaciones y objetos simulados
- Cómo usar la inserción manual de dependencias en Android para pruebas de integración y unidades
- Cómo aplicar el patrón de localizador de servicios
- Cómo probar repositorios, fragmentos, ViewModels y el componente Navigation
Usarás las siguientes bibliotecas y conceptos de código:
Actividades
- Escribe pruebas de unidades para un repositorio con un doble de prueba y una inyección de dependencias.
- Escribe pruebas de unidades para un modelo de vistas con un doble de prueba y una inyección de dependencias.
- Escribe pruebas de integración para fragmentos y sus ViewModels con el framework de pruebas de IU de Espresso.
- Escribe pruebas de navegación con Mockito y Espresso.
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 algunas 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.
Descarga el código
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 end_codelab_1
Tómate un momento para familiarizarte con el código siguiendo las instrucciones que se indican a continuación.
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 este codelab, aprenderás a probar repositorios, modelos de vistas y fragmentos con dobles de prueba y la inserción de dependencias. Antes de analizar qué son, es importante comprender el razonamiento que guiará qué y cómo escribirás estas pruebas.
En esta sección, se abarcan algunas prácticas recomendadas para las pruebas en general, ya que se aplican a Android.
La pirámide de pruebas
Cuando piensas en una estrategia de pruebas, hay tres aspectos relacionados que debes tener en cuenta:
- Alcance: ¿Qué parte del código abarca la prueba? Las pruebas se pueden ejecutar en un solo método, en toda la aplicación o en algún punto intermedio.
- Velocidad: ¿Qué tan rápido se ejecuta la prueba? Las velocidades de las pruebas pueden variar de milisegundos a varios minutos.
- Fidelidad: ¿Qué tan "real" es la prueba? Por ejemplo, si parte del código que estás probando necesita realizar una solicitud de red, ¿el código de prueba realmente realiza esta solicitud de red o simula el resultado? Si la prueba realmente se comunica con la red, significa que tiene mayor fidelidad. La desventaja es que la prueba podría tardar más en ejecutarse, podría generar errores si la red no funciona o podría ser costosa de usar.
Existen compensaciones inherentes entre estos aspectos. Por ejemplo, la velocidad y la fidelidad son un equilibrio: cuanto más rápida es la prueba, generalmente, menos fidelidad tiene, y viceversa. Una forma común de dividir las pruebas automatizadas es en estas tres categorías:
- Pruebas de unidades: Son pruebas muy enfocadas que se ejecutan en una sola clase, por lo general, un solo método en esa clase. Si falla una prueba de unidades, puedes saber exactamente en qué parte del código se encuentra el problema. Tienen baja fidelidad, ya que, en el mundo real, tu app implica mucho más que la ejecución de un método o una clase. Son lo suficientemente rápidas como para ejecutarse cada vez que cambias el código. Por lo general, serán pruebas que se ejecuten de forma local (en el conjunto de fuentes
test). Ejemplo: Prueba de métodos únicos en ViewModels y repositorios - Pruebas de integración: Estas pruebas verifican la interacción de varias clases para asegurarse de que se comporten según lo esperado cuando se usan juntas. Una forma de estructurar las pruebas de integración es hacer que prueben una sola función, como la capacidad de guardar una tarea. Prueban un alcance de código más amplio que las pruebas de unidades, pero aún están optimizadas para ejecutarse rápidamente, en lugar de tener fidelidad completa. Se pueden ejecutar de forma local o como pruebas de instrumentación, según la situación. Ejemplo: Probar toda la funcionalidad de un solo par de fragmento y modelo de vista
- Pruebas de extremo a extremo (E2E): Prueban una combinación de funciones que trabajan juntas. Prueban grandes partes de la app, simulan el uso real de cerca y, por lo tanto, suelen ser lentas. Tienen la mayor fidelidad y te indican que tu aplicación funciona correctamente en su totalidad. En general, estas pruebas serán pruebas instrumentadas (en el conjunto de orígenes
androidTest)
Ejemplo: Iniciar toda la app y probar algunas funciones juntas.
La proporción sugerida de estas pruebas a menudo se representa con una pirámide, en la que la gran mayoría de las pruebas son pruebas de unidades.

Arquitectura y pruebas
Tu capacidad para probar tu app en todos los niveles de la pirámide de pruebas está intrínsecamente vinculada a la arquitectura de la app. Por ejemplo, una aplicación con una arquitectura extremadamente deficiente podría colocar toda su lógica dentro de un solo método. Es posible que puedas escribir una prueba de extremo a extremo para esto, ya que estas pruebas tienden a probar grandes porciones de la app, pero ¿qué sucede con la escritura de pruebas de unidades o de integración? Con todo el código en un solo lugar, es difícil probar solo el código relacionado con una sola unidad o función.
Un mejor enfoque sería dividir la lógica de la aplicación en varios métodos y clases, lo que permitiría probar cada parte de forma aislada. La arquitectura es una forma de dividir y organizar tu código, lo que permite realizar pruebas de unidades y de integración más fácilmente. La app de tareas pendientes que probarás sigue una arquitectura particular:
En esta lección, verás cómo probar partes de la arquitectura anterior de forma aislada:
- Primero, probarás las unidades del repositorio.
- Luego, usarás un doble de prueba en el modelo de vistas, lo que es necesario para realizar pruebas de unidades y pruebas de integración del modelo de vistas.
- A continuación, aprenderás a escribir pruebas de integración para fragmentos y sus modelos de vista.
- Por último, aprenderás a escribir pruebas de integración que incluyan el componente Navigation.
En la próxima lección, se abordarán las pruebas de extremo a extremo.
Cuando escribes una prueba de unidades para una parte de una clase (un método o una pequeña colección de métodos), tu objetivo es probar solo el código de esa clase.
Probar solo el código en una clase o clases específicas puede ser complicado. Veamos un ejemplo. Abre la clase data.source.DefaultTaskRepository en el conjunto de fuentes main. Este es el repositorio de la app y la clase para la que escribirás pruebas de unidades a continuación.
Tu objetivo es probar solo el código de esa clase. Sin embargo, DefaultTaskRepository depende de otras clases, como LocalTaskDataSource y RemoteTaskDataSource, para funcionar. Otra forma de decir esto es que LocalTaskDataSource y RemoteTaskDataSource son dependencias de DefaultTaskRepository.
Por lo tanto, cada método en DefaultTaskRepository llama a métodos en clases de fuentes de datos, que a su vez llaman a métodos en otras clases para guardar información en una base de datos o comunicarse con la red.

Por ejemplo, observa este método en DefaultTasksRepo.
suspend fun getTasks(forceUpdate: Boolean = false): Result<List<Task>> {
if (forceUpdate) {
try {
updateTasksFromRemoteDataSource()
} catch (ex: Exception) {
return Result.Error(ex)
}
}
return tasksLocalDataSource.getTasks()
}getTasks es una de las llamadas más "básicas" que puedes hacer a tu repositorio. Este método incluye la lectura de una base de datos SQLite y la realización de llamadas de red (la llamada a updateTasksFromRemoteDataSource). Esto implica mucho más código que solo el código del repositorio.
Estas son algunas razones más específicas por las que es difícil probar el repositorio:
- Debes pensar en crear y administrar una base de datos para realizar incluso las pruebas más simples de este repositorio. Esto plantea preguntas como "¿debería ser una prueba local o instrumentada?" y si deberías usar AndroidX Test para obtener un entorno de Android simulado.
- Algunas partes del código, como el código de redes, pueden tardar mucho en ejecutarse o, incluso, fallar ocasionalmente, lo que genera pruebas inestables de larga duración.
- Tus pruebas podrían perder la capacidad de diagnosticar qué código es el responsable de una falla en la prueba. Tus pruebas podrían comenzar a probar código que no es de repositorio, por lo que, por ejemplo, tus supuestas pruebas de unidades de "repositorio" podrían fallar debido a un problema en parte del código dependiente, como el código de la base de datos.
Dobles de prueba
La solución a esto es que, cuando pruebes el repositorio, no uses el código real de red o de base de datos, sino que uses un doble de prueba. Un doble de prueba es una versión de una clase creada específicamente para las pruebas. Está diseñada para reemplazar la versión real de una clase en las pruebas. Es similar a cómo un doble de riesgo es un actor que se especializa en acrobacias y reemplaza al actor real en acciones peligrosas.
Estos son algunos tipos de dobles de prueba:
Falso | Es un doble de prueba que tiene una implementación "en funcionamiento" de la clase, pero se implementa de una manera que la hace adecuada para las pruebas, pero no para la producción. |
Mock | Es un doble de prueba que hace un seguimiento de los métodos que se llamaron. Luego, aprueba o rechaza una prueba según si se llamaron correctamente a sus métodos. |
Stub | Es un doble de prueba que no incluye lógica y solo devuelve lo que programas para que devuelva. Por ejemplo, se podría programar un |
Dummy | Es un doble de prueba que se pasa, pero no se usa, como si solo necesitaras proporcionarlo como parámetro. Si tuvieras un |
Espía | Un doble de prueba que también realiza un seguimiento de cierta información adicional; por ejemplo, si creaste un |
Para obtener más información sobre los simuladores de prueba, consulta Testing on the Toilet: Know Your Test Doubles.
Las pruebas dobles más comunes que se usan en Android son los objetos simulados y los objetos ficticios.
En esta tarea, crearás un doble de prueba FakeDataSource para probar la unidad DefaultTasksRepository desacoplada de las fuentes de datos reales.
Paso 1: Crea la clase FakeDataSource
En este paso, crearás una clase llamada FakeDataSouce, que será un doble de prueba de un LocalDataSource y un RemoteDataSource.
- En el conjunto de fuentes test, haz clic con el botón derecho y selecciona New -> Package.

- Crea un paquete de datos con un paquete de fuente dentro.
- Crea una clase nueva llamada
FakeDataSourceen el paquete data/source.

Paso 2: Implementa la interfaz de TasksDataSource
Para poder usar tu nueva clase FakeDataSource como un doble de prueba, debe poder reemplazar las otras fuentes de datos. Esas fuentes de datos son TasksLocalDataSource y TasksRemoteDataSource.

- Observa cómo ambos implementan la interfaz
TasksDataSource.
class TasksLocalDataSource internal constructor(
private val tasksDao: TasksDao,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) : TasksDataSource { ... }
object TasksRemoteDataSource : TasksDataSource { ... }- Haz que
FakeDataSourceimplementeTasksDataSource:
class FakeDataSource : TasksDataSource {
}Android Studio mostrará un mensaje de error que indica que no implementaste los métodos requeridos para TasksDataSource.
- Usa el menú de corrección rápida y selecciona Implementar miembros.

- Selecciona todos los métodos y presiona Aceptar.

Paso 3: Implementa el método getTasks en FakeDataSource
FakeDataSource es un tipo específico de doble de prueba llamado simulación. Un objeto simulado es un doble de prueba que tiene una implementación "en funcionamiento" de la clase, pero se implementa de una manera que lo hace adecuado para las pruebas, pero no para la producción. La implementación "en funcionamiento" significa que la clase producirá resultados realistas a partir de las entradas.
Por ejemplo, tu fuente de datos falsa no se conectará a la red ni guardará nada en una base de datos, sino que solo usará una lista en la memoria. Esto "funcionará como esperas", ya que los métodos para obtener o guardar tareas devolverán los resultados esperados, pero nunca podrías usar esta implementación en producción, ya que no se guarda en el servidor ni en una base de datos.
A FakeDataSource
- te permite probar el código en
DefaultTasksRepositorysin necesidad de depender de una base de datos o una red reales. - proporciona una implementación "suficientemente real" para las pruebas.
- Cambia el constructor
FakeDataSourcepara crear unvarllamadotasksque sea unMutableList<Task>?con un valor predeterminado de una lista mutable vacía.
class FakeDataSource(var tasks: MutableList<Task>? = mutableListOf()) : TasksDataSource { // Rest of class }
Esta es la lista de tareas que simulan ser una respuesta de base de datos o servidor. Por ahora, el objetivo es probar el método getTasks del repositorio . Esto llama a los métodos getTasks, deleteAllTasks y saveTask de la fuente de datos .
Escribe una versión falsa de estos métodos:
- Escribe
getTasks: Sitasksno esnull, devuelve un resultadoSuccess. Sitasksesnull, devuelve un resultadoError. - Write
deleteAllTasks: Borra la lista de tareas mutables. - Escribe
saveTask: Agrega la tarea a la lista.
Esos métodos, implementados para FakeDataSource, se ven como el siguiente código.
override suspend fun getTasks(): Result<List<Task>> {
tasks?.let { return Success(ArrayList(it)) }
return Error(
Exception("Tasks not found")
)
}
override suspend fun deleteAllTasks() {
tasks?.clear()
}
override suspend fun saveTask(task: Task) {
tasks?.add(task)
}Estas son las sentencias de importación, si las necesitas:
import com.example.android.architecture.blueprints.todoapp.data.Result
import com.example.android.architecture.blueprints.todoapp.data.Result.Error
import com.example.android.architecture.blueprints.todoapp.data.Result.Success
import com.example.android.architecture.blueprints.todoapp.data.TaskEsto es similar a cómo funcionan las fuentes de datos locales y remotas reales.
En este paso, usarás una técnica llamada inserción de dependencias manual para que puedas usar el doble de prueba falso que acabas de crear.
El problema principal es que tienes un FakeDataSource, pero no está claro cómo lo usas en las pruebas. Debe reemplazar TasksRemoteDataSource y TasksLocalDataSource, pero solo en las pruebas. Tanto TasksRemoteDataSource como TasksLocalDataSource son dependencias de DefaultTasksRepository, lo que significa que DefaultTasksRepositories requiere o "depende" de estas clases para ejecutarse.
En este momento, las dependencias se construyen dentro del método init de DefaultTasksRepository.
DefaultTasksRepository.kt
class DefaultTasksRepository private constructor(application: Application) {
private val tasksRemoteDataSource: TasksDataSource
private val tasksLocalDataSource: TasksDataSource
// Some other code
init {
val database = Room.databaseBuilder(application.applicationContext,
ToDoDatabase::class.java, "Tasks.db")
.build()
tasksRemoteDataSource = TasksRemoteDataSource
tasksLocalDataSource = TasksLocalDataSource(database.taskDao())
}
// Rest of class
}Debido a que creas y asignas taskLocalDataSource y tasksRemoteDataSource dentro de DefaultTasksRepository, estos se codifican de forma rígida. No hay forma de intercambiar tu doble de prueba.
En su lugar, lo que debes hacer es proporcionar estas fuentes de datos a la clase, en lugar de codificarlas de forma rígida. Proporcionar dependencias se conoce como inyección de dependencias. Existen diferentes formas de proporcionar dependencias y, por lo tanto, diferentes tipos de inyección de dependencias.
La inserción de dependencias en el constructor te permite intercambiar el doble de prueba pasándolo al constructor.
Sin inyección
| Inyección
|
Paso 1: Usa la inserción de dependencias del constructor en DefaultTasksRepository
- Cambia el constructor de
DefaultTaskRepositorypara que tome unApplicationy las fuentes de datos, y el dispatcher de corrutinas (que también deberás intercambiar para tus pruebas, lo que se describe con más detalle en la sección de la tercera lección sobre corrutinas).
DefaultTasksRepository.kt
// REPLACE
class DefaultTasksRepository private constructor(application: Application) { // Rest of class }
// WITH
class DefaultTasksRepository(
private val tasksRemoteDataSource: TasksDataSource,
private val tasksLocalDataSource: TasksDataSource,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) { // Rest of class }- Como pasaste las dependencias, quita el método
init. Ya no es necesario que crees las dependencias. - También borra las variables de instancia anteriores. Los defines en el constructor:
DefaultTasksRepository.kt
// Delete these old variables
private val tasksRemoteDataSource: TasksDataSource
private val tasksLocalDataSource: TasksDataSource
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO- Por último, actualiza el método
getRepositorypara usar el nuevo constructor:
DefaultTasksRepository.kt
companion object {
@Volatile
private var INSTANCE: DefaultTasksRepository? = null
fun getRepository(app: Application): DefaultTasksRepository {
return INSTANCE ?: synchronized(this) {
val database = Room.databaseBuilder(app,
ToDoDatabase::class.java, "Tasks.db")
.build()
DefaultTasksRepository(TasksRemoteDataSource, TasksLocalDataSource(database.taskDao())).also {
INSTANCE = it
}
}
}
}Ahora estás usando la inserción de dependencias del constructor.
Paso 2: Usa tu FakeDataSource en las pruebas
Ahora que tu código usa la inyección de dependencias del constructor, puedes usar tu fuente de datos falsa para probar tu DefaultTasksRepository.
- Haz clic con el botón derecho en el nombre de la clase
DefaultTasksRepositoryy selecciona Generar y, luego, Prueba. - Sigue las indicaciones para crear
DefaultTasksRepositoryTesten el conjunto de fuentes test. - En la parte superior de la nueva clase
DefaultTasksRepositoryTest, agrega las siguientes variables de miembro para representar los datos en tus fuentes de datos simulados.
DefaultTasksRepositoryTest.kt
private val task1 = Task("Title1", "Description1")
private val task2 = Task("Title2", "Description2")
private val task3 = Task("Title3", "Description3")
private val remoteTasks = listOf(task1, task2).sortedBy { it.id }
private val localTasks = listOf(task3).sortedBy { it.id }
private val newTasks = listOf(task3).sortedBy { it.id }- Crea tres variables, dos variables miembro
FakeDataSource(una para cada fuente de datos de tu repositorio) y una variable para elDefaultTasksRepositoryque probarás.
DefaultTasksRepositoryTest.kt
private lateinit var tasksRemoteDataSource: FakeDataSource
private lateinit var tasksLocalDataSource: FakeDataSource
// Class under test
private lateinit var tasksRepository: DefaultTasksRepositoryCrea un método para configurar e inicializar un DefaultTasksRepository que se pueda probar. Este DefaultTasksRepository usará tu doble de prueba, FakeDataSource.
- Crea un método llamado
createRepositoryy anótalo con@Before. - Crea instancias de tus fuentes de datos falsas con las listas
remoteTasksylocalTasks. - Crea una instancia de tu
tasksRepositorycon las dos fuentes de datos falsas que acabas de crear yDispatchers.Unconfined.
El método final debería verse como el siguiente código.
DefaultTasksRepositoryTest.kt
@Before
fun createRepository() {
tasksRemoteDataSource = FakeDataSource(remoteTasks.toMutableList())
tasksLocalDataSource = FakeDataSource(localTasks.toMutableList())
// Get a reference to the class under test
tasksRepository = DefaultTasksRepository(
// TODO Dispatchers.Unconfined should be replaced with Dispatchers.Main
// this requires understanding more about coroutines + testing
// so we will keep this as Unconfined for now.
tasksRemoteDataSource, tasksLocalDataSource, Dispatchers.Unconfined
)
}Paso 3: Escribe la prueba de getTasks() de DefaultTasksRepository
Es hora de escribir una prueba de DefaultTasksRepository.
- Escribe una prueba para el método
getTasksdel repositorio. Verifica que, cuando llames agetTaskscontrue(lo que significa que debería volver a cargar desde la fuente de datos remota), se devuelvan datos de la fuente de datos remota (en lugar de la fuente de datos local).
DefaultTasksRepositoryTest.kt
@Test
fun getTasks_requestsAllTasksFromRemoteDataSource(){
// When tasks are requested from the tasks repository
val tasks = tasksRepository.getTasks(true) as Success
// Then tasks are loaded from the remote data source
assertThat(tasks.data, IsEqual(remoteTasks))
}Recibirás un error cuando llames a getTasks:
Paso 4: Agrega runBlockingTest
Se espera el error de corrutina porque getTasks es una función suspend y debes iniciar una corrutina para llamarla. Para ello, necesitas un alcance de corrutinas. Para resolver este error, deberás agregar algunas dependencias de Gradle para controlar el lanzamiento de corrutinas en tus pruebas.
- Agrega las dependencias necesarias para probar corrutinas al conjunto de fuentes de prueba con
testImplementation.
app/build.gradle
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"¡No olvides sincronizar!
kotlinx-coroutines-test es la biblioteca de pruebas de corrutinas, diseñada específicamente para probar corrutinas. Para ejecutar tus pruebas, usa la función runBlockingTest. Esta es una función que proporciona la biblioteca de pruebas de corrutinas. Toma un bloque de código y, luego, lo ejecuta en un contexto de corrutina especial que se ejecuta de forma síncrona e inmediata, lo que significa que las acciones ocurrirán en un orden determinístico. Esto hace que tus corrutinas se ejecuten como si no fueran corrutinas, por lo que está diseñado para probar código.
Usa runBlockingTest en tus clases de prueba cuando llames a una función suspend. Aprenderás más sobre cómo funciona runBlockingTest y cómo probar corrutinas en el siguiente codelab de esta serie.
- Agrega
@ExperimentalCoroutinesApiarriba de la clase. Esto expresa que sabes que estás usando una API de corrutinas experimental (runBlockingTest) en la clase. De lo contrario, recibirás una advertencia. - De nuevo en tu
DefaultTasksRepositoryTest, agregarunBlockingTestpara que tome toda tu prueba como un "bloque" de código.
Esta prueba final se ve como el siguiente código.
DefaultTasksRepositoryTest.kt
import com.example.android.architecture.blueprints.todoapp.data.Result.Success
import com.example.android.architecture.blueprints.todoapp.data.Task
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runBlockingTest
import org.hamcrest.core.IsEqual
import org.junit.Assert.*
import org.junit.Before
import org.junit.Test
@ExperimentalCoroutinesApi
class DefaultTasksRepositoryTest {
private val task1 = Task("Title1", "Description1")
private val task2 = Task("Title2", "Description2")
private val task3 = Task("Title3", "Description3")
private val remoteTasks = listOf(task1, task2).sortedBy { it.id }
private val localTasks = listOf(task3).sortedBy { it.id }
private val newTasks = listOf(task3).sortedBy { it.id }
private lateinit var tasksRemoteDataSource: FakeDataSource
private lateinit var tasksLocalDataSource: FakeDataSource
// Class under test
private lateinit var tasksRepository: DefaultTasksRepository
@Before
fun createRepository() {
tasksRemoteDataSource = FakeDataSource(remoteTasks.toMutableList())
tasksLocalDataSource = FakeDataSource(localTasks.toMutableList())
// Get a reference to the class under test
tasksRepository = DefaultTasksRepository(
// TODO Dispatchers.Unconfined should be replaced with Dispatchers.Main
// this requires understanding more about coroutines + testing
// so we will keep this as Unconfined for now.
tasksRemoteDataSource, tasksLocalDataSource, Dispatchers.Unconfined
)
}
@Test
fun getTasks_requestsAllTasksFromRemoteDataSource() = runBlockingTest {
// When tasks are requested from the tasks repository
val tasks = tasksRepository.getTasks(true) as Success
// Then tasks are loaded from the remote data source
assertThat(tasks.data, IsEqual(remoteTasks))
}
}- Ejecuta tu nueva prueba de
getTasks_requestsAllTasksFromRemoteDataSourcey confirma que funciona y que el error desapareció.
Acabas de ver cómo realizar pruebas de unidades en un repositorio. En los próximos pasos, volverás a usar la inyección de dependencias y crearás otro doble de prueba, esta vez para mostrar cómo escribir pruebas de unidades y de integración para tus modelos de vistas.
Las pruebas de unidades solo deben probar la clase o el método que te interesa. Esto se conoce como pruebas de aislamiento, en las que aíslas claramente tu "unidad" y solo pruebas el código que forma parte de esa unidad.
Por lo tanto, TasksViewModelTest solo debe probar el código de TasksViewModel, no debe probar las clases de base de datos, red o repositorio. Por lo tanto, para tus modelos de vistas, al igual que lo hiciste con tu repositorio, crearás un repositorio falso y aplicarás la inyección de dependencias para usarlo en tus pruebas.
En esta tarea, aplicarás la inyección de dependencias a los modelos de vistas.

Paso 1: Cómo crear una interfaz de TasksRepository
El primer paso para usar la inyección de dependencias del constructor es crear una interfaz común que compartan la clase falsa y la real.
¿Cómo se ve esto en la práctica? Observa TasksRemoteDataSource, TasksLocalDataSource y FakeDataSource, y nota que todos comparten la misma interfaz: TasksDataSource. Esto te permite indicar en el constructor de DefaultTasksRepository que tomas un TasksDataSource.
DefaultTasksRepository.kt
class DefaultTasksRepository(
private val tasksRemoteDataSource: TasksDataSource,
private val tasksLocalDataSource: TasksDataSource,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) {Esto es lo que nos permite intercambiar tu FakeDataSource.
A continuación, crea una interfaz para DefaultTasksRepository, como lo hiciste para las fuentes de datos. Debe incluir todos los métodos públicos (superficie de la API pública) de DefaultTasksRepository.
- Abre
DefaultTasksRepositoryy haz clic con el botón derecho en el nombre de la clase. Luego, selecciona Refactor -> Extract -> Interface.

- Elige Extraer en un archivo independiente.

- En la ventana Extract Interface, cambia el nombre de la interfaz a
TasksRepository. - En la sección Members to form interface, marca todos los miembros excepto los dos miembros complementarios y los métodos privados.

- Haz clic en Refactor. La nueva interfaz
TasksRepositorydebería aparecer en el paquete data/source .

Además, DefaultTasksRepository ahora implementa TasksRepository.
- Ejecuta tu app (no las pruebas) para asegurarte de que todo siga funcionando correctamente.
Paso 2: Crea FakeTestRepository
Ahora que tienes la interfaz, puedes crear el doble de prueba DefaultTaskRepository.
- En el conjunto de fuentes test, en data/source, crea el archivo y la clase de Kotlin
FakeTestRepository.kty extiéndelos desde la interfazTasksRepository.
FakeTestRepository.kt
class FakeTestRepository : TasksRepository {
}Se te indicará que debes implementar los métodos de la interfaz.
- Coloca el cursor sobre el error hasta que veas el menú de sugerencias y, luego, haz clic en Implement members y selecciónalo.
- Selecciona todos los métodos y presiona Aceptar.

Paso 3: Implementa métodos de FakeTestRepository
Ahora tienes una clase FakeTestRepository con métodos "no implementados". De manera similar a como implementaste FakeDataSource, FakeTestRepository se respaldará con una estructura de datos, en lugar de lidiar con una mediación complicada entre fuentes de datos locales y remotas.
Ten en cuenta que tu FakeTestRepository no necesita usar FakeDataSources ni nada parecido; solo necesita devolver resultados falsos realistas a partir de las entradas. Usarás un LinkedHashMap para almacenar la lista de tareas y un MutableLiveData para tus tareas observables.
- En
FakeTestRepository, agrega una variableLinkedHashMapque represente la lista actual de tareas y unMutableLiveDatapara tus tareas observables.
FakeTestRepository.kt
class FakeTestRepository : TasksRepository {
var tasksServiceData: LinkedHashMap<String, Task> = LinkedHashMap()
private val observableTasks = MutableLiveData<Result<List<Task>>>()
// Rest of class
}Implementa los siguientes métodos:
getTasks: Este método debe tomar eltasksServiceDatay convertirlo en una lista contasksServiceData.values.toList()y, luego, devolverlo como un resultadoSuccess.refreshTasks: Actualiza el valor deobservableTaskspara que sea el que devuelvegetTasks().observeTasks: Crea una corrutina conrunBlockingy ejecutarefreshTasks. Luego, devuelveobservableTasks.
A continuación, se muestra el código de esos métodos.
FakeTestRepository.kt
class FakeTestRepository : TasksRepository {
var tasksServiceData: LinkedHashMap<String, Task> = LinkedHashMap()
private val observableTasks = MutableLiveData<Result<List<Task>>>()
override suspend fun getTasks(forceUpdate: Boolean): Result<List<Task>> {
return Result.Success(tasksServiceData.values.toList())
}
override suspend fun refreshTasks() {
observableTasks.value = getTasks()
}
override fun observeTasks(): LiveData<Result<List<Task>>> {
runBlocking { refreshTasks() }
return observableTasks
}
// Rest of class
}Paso 4: Agrega un método para probar addTasks
Cuando realices pruebas, es mejor tener algunos Tasks ya en tu repositorio. Podrías llamar a saveTask varias veces, pero, para que sea más fácil, agrega un método de ayuda específicamente para las pruebas que te permita agregar tareas.
- Agrega el método
addTasks, que toma unvarargde tareas, agrega cada una aHashMapy, luego, actualiza las tareas.
FakeTestRepository.kt
fun addTasks(vararg tasks: Task) {
for (task in tasks) {
tasksServiceData[task.id] = task
}
runBlocking { refreshTasks() }
}En este punto, tienes un repositorio falso para realizar pruebas con algunos de los métodos clave implementados. A continuación, úsalo en tus pruebas.
En esta tarea, usarás una clase falsa dentro de un ViewModel. Usa la inyección de dependencias del constructor para incorporar las dos fuentes de datos a través de la inyección de dependencias del constructor agregando una variable TasksRepository al constructor de TasksViewModel.
Este proceso es un poco diferente con los modelos de vistas, ya que no los construyes directamente. Por ejemplo:
class TasksFragment : Fragment() {
private val viewModel by viewModels<TasksViewModel>()
// Rest of class...
}
Como en el código anterior, usas el delegado de propiedad viewModel's que crea el modelo de vista. Para cambiar la forma en que se construye el modelo de vista, deberás agregar y usar un ViewModelProvider.Factory. Si no conoces ViewModelProvider.Factory, puedes obtener más información aquí.
Paso 1: Crea y usa un ViewModelFactory en TasksViewModel
Comienza por actualizar las clases y las pruebas relacionadas con la pantalla Tasks.
- Abrir
TasksViewModel - Cambia el constructor de
TasksViewModelpara que tomeTasksRepositoryen lugar de construirlo dentro de la clase.
TasksViewModel.kt
// REPLACE
class TasksViewModel(application: Application) : AndroidViewModel(application) {
private val tasksRepository = DefaultTasksRepository.getRepository(application)
// Rest of class
}
// WITH
class TasksViewModel( private val tasksRepository: TasksRepository ) : ViewModel() {
// Rest of class
}Como cambiaste el constructor, ahora debes usar una fábrica para construir TasksViewModel. Coloca la clase de fábrica en el mismo archivo que TasksViewModel, pero también puedes colocarla en su propio archivo.
- En la parte inferior del archivo
TasksViewModel, fuera de la clase, agrega unTasksViewModelFactoryque tome unTasksRepositorysimple.
TasksViewModel.kt
@Suppress("UNCHECKED_CAST")
class TasksViewModelFactory (
private val tasksRepository: TasksRepository
) : ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel> create(modelClass: Class<T>) =
(TasksViewModel(tasksRepository) as T)
}
Esta es la forma estándar de cambiar la forma en que se construyen los ViewModel. Ahora que tienes la fábrica, úsala dondequiera que construyas tu modelo de vista.
- Actualiza
TasksFragmentpara usar la fábrica.
TasksFragment.kt
// REPLACE
private val viewModel by viewModels<TasksViewModel>()
// WITH
private val viewModel by viewModels<TasksViewModel> {
TasksViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}- Ejecuta el código de tu app y asegúrate de que todo siga funcionando.
Paso 2: Cómo usar FakeTestRepository dentro de TasksViewModelTest
Ahora, en lugar de usar el repositorio real en las pruebas del modelo de vista, puedes usar el repositorio falso.
- Abre
TasksViewModelTest. - Agrega una propiedad
FakeTestRepositoryen elTasksViewModelTest.
TaskViewModelTest.kt
@RunWith(AndroidJUnit4::class)
class TasksViewModelTest {
// Use a fake repository to be injected into the viewmodel
private lateinit var tasksRepository: FakeTestRepository
// Rest of class
}- Actualiza el método
setupViewModelpara crear unFakeTestRepositorycon tres tareas y, luego, construye eltasksViewModelcon este repositorio.
TasksViewModelTest.kt
@Before
fun setupViewModel() {
// We initialise the tasks to 3, with one active and two completed
tasksRepository = FakeTestRepository()
val task1 = Task("Title1", "Description1")
val task2 = Task("Title2", "Description2", true)
val task3 = Task("Title3", "Description3", true)
tasksRepository.addTasks(task1, task2, task3)
tasksViewModel = TasksViewModel(tasksRepository)
}- Como ya no usas el código AndroidX Test
ApplicationProvider.getApplicationContext, también puedes quitar la anotación@RunWith(AndroidJUnit4::class). - Ejecuta tus pruebas y asegúrate de que todas sigan funcionando.
Con la inyección de dependencias del constructor, quitaste DefaultTasksRepository como dependencia y lo reemplazaste por tu FakeTestRepository en las pruebas.
Paso 3: También actualiza el fragmento y el ViewModel de TaskDetail
Realiza los mismos cambios para TaskDetailFragment y TaskDetailViewModel. Esto preparará el código para cuando escribas pruebas de TaskDetail a continuación.
- Abrir
TaskDetailViewModel - Actualiza el constructor:
TaskDetailViewModel.kt
// REPLACE
class TaskDetailViewModel(application: Application) : AndroidViewModel(application) {
private val tasksRepository = DefaultTasksRepository.getRepository(application)
// Rest of class
}
// WITH
class TaskDetailViewModel(
private val tasksRepository: TasksRepository
) : ViewModel() { // Rest of class }- En la parte inferior del archivo
TaskDetailViewModel, fuera de la clase, agrega unTaskDetailViewModelFactory.
TaskDetailViewModel.kt
@Suppress("UNCHECKED_CAST")
class TaskDetailViewModelFactory (
private val tasksRepository: TasksRepository
) : ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel> create(modelClass: Class<T>) =
(TaskDetailViewModel(tasksRepository) as T)
}- Actualiza
TasksFragmentpara usar la fábrica.
TasksFragment.kt
// REPLACE
private val viewModel by viewModels<TaskDetailViewModel>()
// WITH
private val viewModel by viewModels<TaskDetailViewModel> {
TaskDetailViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}- Ejecuta tu código y asegúrate de que todo funcione correctamente.
Ahora puedes usar un FakeTestRepository en lugar del repositorio real en TasksFragment y TasksDetailFragment.
A continuación, escribirás pruebas de integración para probar las interacciones de tu fragmento y ViewModel. Descubrirás si el código de tu modelo de vista actualiza la IU de forma adecuada. Para ello, usa
- El patrón ServiceLocator
- las bibliotecas de Espresso y Mockito
Las pruebas de integración prueban la interacción de varias clases para asegurarse de que se comporten según lo esperado cuando se usan juntas. Estas pruebas se pueden ejecutar de forma local (conjunto de fuentes test) o como pruebas de instrumentación (conjunto de fuentes androidTest).

En tu caso, tomarás cada fragmento y escribirás pruebas de integración para el fragmento y el modelo de vista para probar las funciones principales del fragmento.
Paso 1: Agrega dependencias de Gradle
- Agrega las siguientes dependencias de Gradle.
app/build.gradle
// Dependencies for Android instrumented unit tests
androidTestImplementation "junit:junit:$junitVersion"
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"
// Testing code should not be included in the main code.
// Once https://issuetracker.google.com/128612536 is fixed this can be fixed.
implementation "androidx.fragment:fragment-testing:$fragmentVersion"
implementation "androidx.test:core:$androidXTestCoreVersion"
Estas dependencias incluyen lo siguiente:
junit:junit: JUnit, que es necesario para escribir instrucciones de prueba básicas.androidx.test:core: Biblioteca principal de pruebas de AndroidXkotlinx-coroutines-test: Biblioteca de pruebas de corrutinasandroidx.fragment:fragment-testing: Biblioteca de prueba de AndroidX para crear fragmentos en pruebas y cambiar su estado.
Como usarás estas bibliotecas en tu conjunto de fuentes androidTest, usa androidTestImplementation para agregarlas como dependencias.
Paso 2: Cómo crear una clase TaskDetailFragmentTest
El objeto TaskDetailFragment muestra información sobre una sola tarea.

Comenzarás escribiendo una prueba de fragmento para TaskDetailFragment, ya que tiene una funcionalidad bastante básica en comparación con los otros fragmentos.
- Abrir
taskdetail.TaskDetailFragment - Genera una prueba para
TaskDetailFragment, como lo hiciste antes. Acepta las opciones predeterminadas y colócalo en el conjunto de orígenes androidTest (NO en el conjunto de orígenestest).

- Agrega las siguientes anotaciones a la clase
TaskDetailFragmentTest.
TaskDetailFragmentTest.kt
@MediumTest
@RunWith(AndroidJUnit4::class)
class TaskDetailFragmentTest {
}El propósito de estas anotaciones es el siguiente:
@MediumTest: Marca la prueba como una prueba de integración de "tiempo de ejecución medio" (en comparación con las pruebas de unidades@SmallTesty las pruebas de extremo a extremo grandes@LargeTest). Esto te ayuda a agrupar y elegir el tamaño de la prueba que deseas ejecutar.@RunWith(AndroidJUnit4::class): Se usa en cualquier clase que use AndroidX Test.
Paso 3: Cómo iniciar un fragmento desde una prueba
En esta tarea, iniciarás TaskDetailFragment con la biblioteca de pruebas de AndroidX. FragmentScenario es una clase de AndroidX Test que encapsula un fragmento y te brinda control directo sobre el ciclo de vida del fragmento para realizar pruebas. Para escribir pruebas de fragmentos, crea un FragmentScenario para el fragmento que estás probando (TaskDetailFragment).
- Copia esta prueba en
TaskDetailFragmentTest.
TaskDetailFragmentTest.kt
@Test
fun activeTaskDetails_DisplayedInUi() {
// GIVEN - Add active (incomplete) task to the DB
val activeTask = Task("Active Task", "AndroidX Rocks", false)
// WHEN - Details fragment launched to display task
val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)
}
Este código anterior:
- Crea una tarea.
- Crea un
Bundle, que representa los argumentos del fragmento para la tarea que se pasan al fragmento. - La función
launchFragmentInContainercrea unFragmentScenariocon este paquete y un tema.
Esta aún no es una prueba terminada, ya que no afirma nada. Por ahora, ejecuta la prueba y observa qué sucede.
- Esta es una prueba de instrumentación, así que asegúrate de que el emulador o tu dispositivo estén visibles.
- Ejecuta la prueba.
Deberían ocurrir algunas cosas.
- En primer lugar, como se trata de una prueba instrumentada, la prueba se ejecutará en tu dispositivo físico (si está conectado) o en un emulador.
- Debería iniciar el fragmento.
- Observa cómo no navega por ningún otro fragmento ni tiene ningún menú asociado a la actividad: solo es el fragmento.
Por último, observa con atención y notarás que el fragmento dice "No hay datos", ya que no carga correctamente los datos de la tarea.

La prueba debe cargar el TaskDetailFragment (lo que ya hiciste) y confirmar que los datos se cargaron correctamente. ¿Por qué no hay datos? Esto se debe a que creaste una tarea, pero no la guardaste en el repositorio.
@Test
fun activeTaskDetails_DisplayedInUi() {
// This DOES NOT save the task anywhere
val activeTask = Task("Active Task", "AndroidX Rocks", false)
val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)
}
Tienes este FakeTestRepository, pero necesitas alguna forma de reemplazar tu repositorio real por el falso para tu fragmento. Lo harás a continuación.
En esta tarea, proporcionarás tu repositorio falso a tu fragmento con un ServiceLocator. Esto te permitirá escribir tus pruebas de integración de fragmentos y modelos de vistas.
Aquí no puedes usar la inyección de dependencias del constructor, como hiciste antes, cuando necesitabas proporcionar una dependencia al ViewModel o al repositorio. La inserción de dependencias en el constructor requiere que construyas la clase. Los fragmentos y las actividades son ejemplos de clases que no construyes y a cuyo constructor generalmente no tienes acceso.
Como no construyes el fragmento, no puedes usar la inserción de dependencias del constructor para intercambiar el doble de prueba del repositorio (FakeTestRepository) al fragmento. En su lugar, usa el patrón Service Locator. El patrón del localizador de servicios es una alternativa a la inyección de dependencias. Implica crear una clase singleton llamada "Service Locator", cuyo propósito es proporcionar dependencias, tanto para el código normal como para el de prueba. En el código de la app normal (el conjunto de fuentes main), todas estas dependencias son las dependencias de la app normal. Para las pruebas, modificas el localizador de servicios para proporcionar versiones de simulacros de prueba de las dependencias.
No se usa el localizador de servicios
| Cómo usar un localizador de servicios
|
Para esta app del codelab, haz lo siguiente:
- Crea una clase de Service Locator que pueda construir y almacenar un repositorio. De forma predeterminada, construye un repositorio "normal".
- Refactoriza tu código para que, cuando necesites un repositorio, uses el localizador de servicios.
- En tu clase de prueba, llama a un método en el localizador de servicios que intercambie el repositorio "normal" con tu doble de prueba.
Paso 1: Crea el ServiceLocator
Creemos una clase ServiceLocator. Se ubicará en el conjunto de fuentes principal con el resto del código de la app, ya que lo usa el código de la aplicación principal.
Nota: ServiceLocator es un singleton, por lo que debes usar la palabra clave object de Kotlin para la clase.
- Crea el archivo ServiceLocator.kt en el nivel superior del conjunto de orígenes principal.
- Define un
objectllamadoServiceLocator. - Crea variables de instancia
databaseyrepository, y establece ambas ennull. - Anota el repositorio con
@Volatileporque varios subprocesos podrían usarlo (@Volatilese explica en detalle aquí).
El código debería verse como se muestra a continuación.
object ServiceLocator {
private var database: ToDoDatabase? = null
@Volatile
var tasksRepository: TasksRepository? = null
}Por el momento, lo único que debe hacer tu ServiceLocator es saber cómo devolver un TasksRepository. Devolverá un DefaultTasksRepository existente o creará y devolverá uno nuevo, si es necesario.DefaultTasksRepository
Define las siguientes funciones:
provideTasksRepository: Proporciona un repositorio existente o crea uno nuevo. Este método debe sersynchronizedenthispara evitar, en situaciones con varios subprocesos en ejecución, crear accidentalmente dos instancias del repositorio.createTasksRepository: Código para crear un repositorio nuevo. Llamará acreateTaskLocalDataSourcey creará un nuevoTasksRemoteDataSource.createTaskLocalDataSource: Es el código para crear una nueva fuente de datos local. Llamará alcreateDataBase.createDataBase: Es el código para crear una base de datos nueva.
A continuación, se incluye el código completo.
ServiceLocator.kt
object ServiceLocator {
private var database: ToDoDatabase? = null
@Volatile
var tasksRepository: TasksRepository? = null
fun provideTasksRepository(context: Context): TasksRepository {
synchronized(this) {
return tasksRepository ?: createTasksRepository(context)
}
}
private fun createTasksRepository(context: Context): TasksRepository {
val newRepo = DefaultTasksRepository(TasksRemoteDataSource, createTaskLocalDataSource(context))
tasksRepository = newRepo
return newRepo
}
private fun createTaskLocalDataSource(context: Context): TasksDataSource {
val database = database ?: createDataBase(context)
return TasksLocalDataSource(database.taskDao())
}
private fun createDataBase(context: Context): ToDoDatabase {
val result = Room.databaseBuilder(
context.applicationContext,
ToDoDatabase::class.java, "Tasks.db"
).build()
database = result
return result
}
}Paso 2: Cómo usar ServiceLocator en Application
Realizarás un cambio en el código principal de la aplicación (no en las pruebas) para crear el repositorio en un solo lugar, tu ServiceLocator.
Es importante que solo crees una instancia de la clase del repositorio. Para garantizar esto, usarás el localizador de servicios en la clase Application.
- En el nivel superior de la jerarquía de paquetes, abre
TodoApplicationy crea unvalpara tu repositorio, y asígnale un repositorio que se obtenga conServiceLocator.provideTaskRepository.
TodoApplication.kt
class TodoApplication : Application() {
val taskRepository: TasksRepository
get() = ServiceLocator.provideTasksRepository(this)
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) Timber.plant(DebugTree())
}
}
Ahora que creaste un repositorio en la aplicación, puedes quitar el método getRepository anterior en DefaultTasksRepository.
- Abre
DefaultTasksRepositoryy borra el objeto complementario.
DefaultTasksRepository.kt
// DELETE THIS COMPANION OBJECT
companion object {
@Volatile
private var INSTANCE: DefaultTasksRepository? = null
fun getRepository(app: Application): DefaultTasksRepository {
return INSTANCE ?: synchronized(this) {
val database = Room.databaseBuilder(app,
ToDoDatabase::class.java, "Tasks.db")
.build()
DefaultTasksRepository(TasksRemoteDataSource, TasksLocalDataSource(database.taskDao())).also {
INSTANCE = it
}
}
}
}Ahora, en todos los lugares donde usabas getRepository, usa el taskRepository de la aplicación. Esto garantiza que, en lugar de crear el repositorio directamente, obtengas el repositorio que proporcionó ServiceLocator.
- Abre
TaskDetailFragementy busca la llamada agetRepositoryen la parte superior de la clase. - Reemplaza esta llamada por una que obtenga el repositorio de
TodoApplication.
TaskDetailFragment.kt
// REPLACE this code
private val viewModel by viewModels<TaskDetailViewModel> {
TaskDetailViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}
// WITH this code
private val viewModel by viewModels<TaskDetailViewModel> {
TaskDetailViewModelFactory((requireContext().applicationContext as TodoApplication).taskRepository)
}- Haz lo mismo con
TasksFragment.
TasksFragment.kt
// REPLACE this code
private val viewModel by viewModels<TasksViewModel> {
TasksViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}
// WITH this code
private val viewModel by viewModels<TasksViewModel> {
TasksViewModelFactory((requireContext().applicationContext as TodoApplication).taskRepository)
}- En el caso de
StatisticsViewModelyAddEditTaskViewModel, actualiza el código que adquiere el repositorio para usar el repositorio deTodoApplication.
TasksFragment.kt
// REPLACE this code
private val tasksRepository = DefaultTasksRepository.getRepository(application)
// WITH this code
private val tasksRepository = (application as TodoApplication).taskRepository
- Ejecuta tu aplicación (no la prueba).
Dado que solo refactorizaste el código, la app debería ejecutarse sin problemas.
Paso 3: Create FakeAndroidTestRepository
Ya tienes un FakeTestRepository en el conjunto de fuentes de prueba. De forma predeterminada, no puedes compartir clases de prueba entre los conjuntos de orígenes test y androidTest. Por lo tanto, debes crear una clase FakeTestRepository duplicada en el conjunto de fuentes androidTest y llamarla FakeAndroidTestRepository.
- Haz clic con el botón derecho en el conjunto de fuentes
androidTesty crea un paquete de datos. Vuelve a hacer clic con el botón derecho y crea un paquete de fuente . - Crea una clase nueva en este paquete fuente llamada
FakeAndroidTestRepository.kt. - Copia el siguiente código en esa clase.
FakeAndroidTestRepository.kt
import androidx.annotation.VisibleForTesting
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.map
import com.example.android.architecture.blueprints.todoapp.data.Result
import com.example.android.architecture.blueprints.todoapp.data.Result.Error
import com.example.android.architecture.blueprints.todoapp.data.Result.Success
import com.example.android.architecture.blueprints.todoapp.data.Task
import kotlinx.coroutines.runBlocking
import java.util.LinkedHashMap
class FakeAndroidTestRepository : TasksRepository {
var tasksServiceData: LinkedHashMap<String, Task> = LinkedHashMap()
private var shouldReturnError = false
private val observableTasks = MutableLiveData<Result<List<Task>>>()
fun setReturnError(value: Boolean) {
shouldReturnError = value
}
override suspend fun refreshTasks() {
observableTasks.value = getTasks()
}
override suspend fun refreshTask(taskId: String) {
refreshTasks()
}
override fun observeTasks(): LiveData<Result<List<Task>>> {
runBlocking { refreshTasks() }
return observableTasks
}
override fun observeTask(taskId: String): LiveData<Result<Task>> {
runBlocking { refreshTasks() }
return observableTasks.map { tasks ->
when (tasks) {
is Result.Loading -> Result.Loading
is Error -> Error(tasks.exception)
is Success -> {
val task = tasks.data.firstOrNull() { it.id == taskId }
?: return@map Error(Exception("Not found"))
Success(task)
}
}
}
}
override suspend fun getTask(taskId: String, forceUpdate: Boolean): Result<Task> {
if (shouldReturnError) {
return Error(Exception("Test exception"))
}
tasksServiceData[taskId]?.let {
return Success(it)
}
return Error(Exception("Could not find task"))
}
override suspend fun getTasks(forceUpdate: Boolean): Result<List<Task>> {
if (shouldReturnError) {
return Error(Exception("Test exception"))
}
return Success(tasksServiceData.values.toList())
}
override suspend fun saveTask(task: Task) {
tasksServiceData[task.id] = task
}
override suspend fun completeTask(task: Task) {
val completedTask = Task(task.title, task.description, true, task.id)
tasksServiceData[task.id] = completedTask
}
override suspend fun completeTask(taskId: String) {
// Not required for the remote data source.
throw NotImplementedError()
}
override suspend fun activateTask(task: Task) {
val activeTask = Task(task.title, task.description, false, task.id)
tasksServiceData[task.id] = activeTask
}
override suspend fun activateTask(taskId: String) {
throw NotImplementedError()
}
override suspend fun clearCompletedTasks() {
tasksServiceData = tasksServiceData.filterValues {
!it.isCompleted
} as LinkedHashMap<String, Task>
}
override suspend fun deleteTask(taskId: String) {
tasksServiceData.remove(taskId)
refreshTasks()
}
override suspend fun deleteAllTasks() {
tasksServiceData.clear()
refreshTasks()
}
fun addTasks(vararg tasks: Task) {
for (task in tasks) {
tasksServiceData[task.id] = task
}
runBlocking { refreshTasks() }
}
}
Paso 4: Prepara tu ServiceLocator para las pruebas
Bien, es hora de usar ServiceLocator para intercambiar dobles de prueba durante las pruebas. Para ello, debes agregar código a tu código de ServiceLocator.
- Abrir
ServiceLocator.kt - Marca el método setter para
tasksRepositorycomo@VisibleForTesting. Esta anotación es una forma de expresar que el motivo por el que el setter es público se debe a las pruebas.
ServiceLocator.kt
@Volatile
var tasksRepository: TasksRepository? = null
@VisibleForTesting setYa sea que ejecutes la prueba sola o en un grupo de pruebas, estas deben ejecutarse exactamente de la misma manera. Esto significa que tus pruebas no deben tener ningún comportamiento que dependa de otro (lo que implica evitar compartir objetos entre pruebas).
Dado que ServiceLocator es un singleton, existe la posibilidad de que se comparta accidentalmente entre las pruebas. Para evitar esto, crea un método que restablezca correctamente el estado de ServiceLocator entre las pruebas.
- Agrega una variable de instancia llamada
lockcon el valorAny.
ServiceLocator.kt
private val lock = Any()- Agrega un método específico para pruebas llamado
resetRepositoryque borre la base de datos y establezca el repositorio y la base de datos como nulos.
ServiceLocator.kt
@VisibleForTesting
fun resetRepository() {
synchronized(lock) {
runBlocking {
TasksRemoteDataSource.deleteAllTasks()
}
// Clear all data to avoid test pollution.
database?.apply {
clearAllTables()
close()
}
database = null
tasksRepository = null
}
}Paso 5: Usa tu ServiceLocator
En este paso, usarás ServiceLocator.
- Abrir
TaskDetailFragmentTest - Declara una variable
lateinit TasksRepository. - Agrega un método de configuración y un método de desmontaje para configurar un
FakeAndroidTestRepositoryantes de cada prueba y limpiarlo después de cada prueba.
TaskDetailFragmentTest.kt
private lateinit var repository: TasksRepository
@Before
fun initRepository() {
repository = FakeAndroidTestRepository()
ServiceLocator.tasksRepository = repository
}
@After
fun cleanupDb() = runBlockingTest {
ServiceLocator.resetRepository()
}
- Encapsula el cuerpo de la función
activeTaskDetails_DisplayedInUi()enrunBlockingTest. - Guarda
activeTasken el repositorio antes de iniciar el fragmento.
repository.saveTask(activeTask)La prueba final se ve como el siguiente código.
TaskDetailFragmentTest.kt
@Test
fun activeTaskDetails_DisplayedInUi() = runBlockingTest{
// GIVEN - Add active (incomplete) task to the DB
val activeTask = Task("Active Task", "AndroidX Rocks", false)
repository.saveTask(activeTask)
// WHEN - Details fragment launched to display task
val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)
}- Anota toda la clase con
@ExperimentalCoroutinesApi.
Cuando termines, el código se verá de la siguiente manera.
TaskDetailFragmentTest.kt
@MediumTest
@ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class)
class TaskDetailFragmentTest {
private lateinit var repository: TasksRepository
@Before
fun initRepository() {
repository = FakeAndroidTestRepository()
ServiceLocator.tasksRepository = repository
}
@After
fun cleanupDb() = runBlockingTest {
ServiceLocator.resetRepository()
}
@Test
fun activeTaskDetails_DisplayedInUi() = runBlockingTest{
// GIVEN - Add active (incomplete) task to the DB
val activeTask = Task("Active Task", "AndroidX Rocks", false)
repository.saveTask(activeTask)
// WHEN - Details fragment launched to display task
val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)
}
}
- Ejecuta la prueba
activeTaskDetails_DisplayedInUi().
Al igual que antes, deberías ver el fragmento, pero esta vez, como configuraste el repositorio correctamente, ahora muestra la información de la tarea.

En este paso, usarás la biblioteca de pruebas de IU de Espresso para completar tu primera prueba de integración. Estructuraste tu código para poder agregar pruebas con aserciones para tu IU. Para ello, usarás la biblioteca de pruebas de Espresso.
Espresso te ayuda a hacer lo siguiente:
- Interactuar con vistas, como hacer clic en botones, deslizar una barra o desplazarse hacia abajo en una pantalla
- Afirma que ciertas vistas están en la pantalla o en un estado determinado (por ejemplo, que contienen texto específico o que una casilla de verificación está marcada, etc.).
Paso 1: Nota sobre la dependencia de Gradle
Ya tendrás la dependencia principal de Espresso, ya que se incluye en los proyectos de Android de forma predeterminada.
app/build.gradle
dependencies {
// ALREADY in your code
androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
// Other dependencies
}androidx.test.espresso:espresso-core: Esta dependencia principal de Espresso se incluye de forma predeterminada cuando creas un nuevo proyecto de Android. Contiene el código de prueba básico para la mayoría de las vistas y las acciones en ellas.
Paso 2: Cómo desactivar las animaciones
Las pruebas de Espresso se ejecutan en un dispositivo real y, por lo tanto, son pruebas de instrumentación por naturaleza. Un problema que surge son las animaciones: si una animación se retrasa y tratas de probar si una vista está en la pantalla, pero aún se está animando, Espresso puede fallar accidentalmente una prueba. Esto puede hacer que las pruebas de Espresso sean inestables.
Para las pruebas de IU de Espresso, se recomienda desactivar las animaciones (además, la prueba se ejecutará más rápido):
- En el dispositivo de prueba, ve a Configuración > Opciones para desarrolladores.
- Inhabilita estos tres parámetros de configuración: Escala de animación de ventana, Escala de animación de transición y Escala de duración de animador.

Paso 3: Cómo ver una prueba de Espresso
Antes de escribir una prueba de Espresso, observa un poco de código de Espresso.
onView(withId(R.id.task_detail_complete_checkbox)).perform(click()).check(matches(isChecked()))Lo que hace esta instrucción es buscar la vista de casilla de verificación con el ID task_detail_complete_checkbox, hacer clic en ella y, luego, confirmar que está marcada.
La mayoría de las sentencias de Espresso se componen de cuatro partes:
1. Método estático de Espresso
onViewonView es un ejemplo de un método estático de Espresso que inicia una instrucción de Espresso. onView es una de las más comunes, pero hay otras opciones, como onData.
2. ViewMatcher
withId(R.id.task_detail_title_text)withId es un ejemplo de un ViewMatcher que obtiene una vista por su ID. Hay otros comparadores de vistas que puedes consultar en la documentación.
3. ViewAction
perform(click())El método perform que toma un ViewAction. Una ViewAction es algo que se puede hacer en la vista, por ejemplo, hacer clic en ella.
check(matches(isChecked()))check, que toma un ViewAssertion ViewAssertions verifica o afirma algo sobre la vista. La ViewAssertion más común que usarás es la aserción matches. Para finalizar la aserción, usa otro ViewMatcher, en este caso isChecked.

Ten en cuenta que no siempre llamas a perform y check en una instrucción de Espresso. Puedes tener instrucciones que solo realicen una aserción con check o que solo realicen una ViewAction con perform.
- Abrir
TaskDetailFragmentTest.kt - Actualiza la prueba
activeTaskDetails_DisplayedInUi.
TaskDetailFragmentTest.kt
@Test
fun activeTaskDetails_DisplayedInUi() = runBlockingTest{
// GIVEN - Add active (incomplete) task to the DB
val activeTask = Task("Active Task", "AndroidX Rocks", false)
repository.saveTask(activeTask)
// WHEN - Details fragment launched to display task
val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)
// THEN - Task details are displayed on the screen
// make sure that the title/description are both shown and correct
onView(withId(R.id.task_detail_title_text)).check(matches(isDisplayed()))
onView(withId(R.id.task_detail_title_text)).check(matches(withText("Active Task")))
onView(withId(R.id.task_detail_description_text)).check(matches(isDisplayed()))
onView(withId(R.id.task_detail_description_text)).check(matches(withText("AndroidX Rocks")))
// and make sure the "active" checkbox is shown unchecked
onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isDisplayed()))
onView(withId(R.id.task_detail_complete_checkbox)).check(matches(not(isChecked())))
}
Estas son las sentencias de importación, si las necesitas:
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isChecked
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import org.hamcrest.core.IsNot.not- Todo lo que aparece después del comentario
// THENusa Espresso. Examina la estructura de la prueba y el uso dewithId, y verifica que se hagan aserciones sobre cómo debería verse la página de detalles. - Ejecuta la prueba y confirma que se complete con éxito.
Paso 4: Opcional: Escribe tu propia prueba de Espresso
Ahora escribe una prueba por tu cuenta.
- Crea una prueba nueva llamada
completedTaskDetails_DisplayedInUiy copia este código de esqueleto.
TaskDetailFragmentTest.kt
@Test
fun completedTaskDetails_DisplayedInUi() = runBlockingTest{
// GIVEN - Add completed task to the DB
// WHEN - Details fragment launched to display task
// THEN - Task details are displayed on the screen
// make sure that the title/description are both shown and correct
}- Observa la prueba anterior y completa esta prueba.
- Ejecuta la prueba y confirma que se complete con éxito.
El completedTaskDetails_DisplayedInUi terminado debería verse como este código.
TaskDetailFragmentTest.kt
@Test
fun completedTaskDetails_DisplayedInUi() = runBlockingTest{
// GIVEN - Add completed task to the DB
val completedTask = Task("Completed Task", "AndroidX Rocks", true)
repository.saveTask(completedTask)
// WHEN - Details fragment launched to display task
val bundle = TaskDetailFragmentArgs(completedTask.id).toBundle()
launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)
// THEN - Task details are displayed on the screen
// make sure that the title/description are both shown and correct
onView(withId(R.id.task_detail_title_text)).check(matches(isDisplayed()))
onView(withId(R.id.task_detail_title_text)).check(matches(withText("Completed Task")))
onView(withId(R.id.task_detail_description_text)).check(matches(isDisplayed()))
onView(withId(R.id.task_detail_description_text)).check(matches(withText("AndroidX Rocks")))
// and make sure the "active" checkbox is shown unchecked
onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isDisplayed()))
onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isChecked()))
}En este último paso, aprenderás a probar el componente de Navigation con un tipo diferente de doble de prueba llamado simulacro y la biblioteca de pruebas Mockito.
En este codelab, usaste un doble de prueba llamado falso. Los objetos simulados son uno de los muchos tipos de dobles de prueba. ¿Qué doble de prueba deberías usar para probar el componente de Navigation?
Piensa en cómo se produce la navegación. Imagina que presionas una de las tareas en TasksFragment para navegar a una pantalla de detalles de la tarea.

Aquí tienes código en TasksFragment que navega a una pantalla de detalles de la tarea cuando se presiona.
TasksFragment.kt
private fun openTaskDetails(taskId: String) {
val action = TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment(taskId)
findNavController().navigate(action)
}
La navegación se produce debido a una llamada al método navigate. Si necesitas escribir una instrucción assert, no hay una forma sencilla de probar si navegaste a TaskDetailFragment. Navegar es una acción complicada que no genera un resultado claro ni un cambio de estado, más allá de la inicialización de TaskDetailFragment.
Lo que puedes afirmar es que se llamó al método navigate con el parámetro de acción correcto. Esto es exactamente lo que hace un doble de prueba simulado: verifica si se llamaron métodos específicos.
Mockito es un framework para crear dobles de prueba. Si bien la palabra "simulación" se usa en la API y en el nombre, no se usa solo para crear simulaciones. También puede crear stubs y spies.
Usarás Mockito para crear un objeto NavigationController simulado que pueda confirmar que se llamó al método de navegación correctamente.
Paso 1: Agrega dependencias de Gradle
- Agrega las dependencias de Gradle.
app/build.gradle
// Dependencies for Android instrumented unit tests
androidTestImplementation "org.mockito:mockito-core:$mockitoVersion"
androidTestImplementation "com.linkedin.dexmaker:dexmaker-mockito:$dexMakerVersion"
androidTestImplementation "androidx.test.espresso:espresso-contrib:$espressoVersion"
org.mockito:mockito-core: Es la dependencia de Mockito.dexmaker-mockito: Esta biblioteca es necesaria para usar Mockito en un proyecto de Android. Mockito necesita generar clases en el tiempo de ejecución. En Android, esto se hace con código de bytes dex, por lo que esta biblioteca permite que Mockito genere objetos durante el tiempo de ejecución en Android.androidx.test.espresso:espresso-contrib: Esta biblioteca se compone de contribuciones externas (de ahí su nombre) que contienen código de prueba para vistas más avanzadas, comoDatePickeryRecyclerView. También contiene verificaciones de accesibilidad y una clase llamadaCountingIdlingResourceque se explica más adelante.
Paso 2: Crea CreateTasksFragmentTest
- Abre
TasksFragment. - Haz clic con el botón derecho en el nombre de la clase
TasksFragmenty selecciona Generar y, luego, Prueba. Crea una prueba en el conjunto de orígenes androidTest. - Copia este código en
TasksFragmentTest.
TasksFragmentTest.kt
@RunWith(AndroidJUnit4::class)
@MediumTest
@ExperimentalCoroutinesApi
class TasksFragmentTest {
private lateinit var repository: TasksRepository
@Before
fun initRepository() {
repository = FakeAndroidTestRepository()
ServiceLocator.tasksRepository = repository
}
@After
fun cleanupDb() = runBlockingTest {
ServiceLocator.resetRepository()
}
}Este código es similar al código de TaskDetailFragmentTest que escribiste. Configura y desconfigura un FakeAndroidTestRepository. Agrega una prueba de navegación para verificar que, cuando haces clic en una tarea de la lista, se te dirija al TaskDetailFragment correcto.
- Agrega la prueba
clickTask_navigateToDetailFragmentOne.
TasksFragmentTest.kt
@Test
fun clickTask_navigateToDetailFragmentOne() = runBlockingTest {
repository.saveTask(Task("TITLE1", "DESCRIPTION1", false, "id1"))
repository.saveTask(Task("TITLE2", "DESCRIPTION2", true, "id2"))
// GIVEN - On the home screen
val scenario = launchFragmentInContainer<TasksFragment>(Bundle(), R.style.AppTheme)
}
- Usa la función
mockde Mockito para crear un objeto simulado.
TasksFragmentTest.kt
val navController = mock(NavController::class.java)Para simular en Mockito, pasa la clase que quieres simular.
A continuación, debes asociar tu NavController con el fragmento. onFragment te permite llamar a métodos en el fragmento.
- Haz que tu nuevo objeto simulado sea el
NavControllerdel fragmento.
scenario.onFragment {
Navigation.setViewNavController(it.view!!, navController)
}- Agrega el código para hacer clic en el elemento del
RecyclerViewque tiene el texto "TITLE1".
// WHEN - Click on the first list item
onView(withId(R.id.tasks_list))
.perform(RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
hasDescendant(withText("TITLE1")), click()))RecyclerViewActions forma parte de la biblioteca espresso-contrib y te permite realizar acciones de Espresso en un RecyclerView.
- Verifica que se haya llamado a
navigatecon el argumento correcto.
// THEN - Verify that we navigate to the first detail screen
verify(navController).navigate(
TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment( "id1")El método verify de Mockito es lo que hace que esto sea una simulación: puedes confirmar que el objeto navController simulado llamó a un método específico (navigate) con un parámetro (actionTasksFragmentToTaskDetailFragment con el ID "id1").
La prueba completa se ve de la siguiente manera:
@Test
fun clickTask_navigateToDetailFragmentOne() = runBlockingTest {
repository.saveTask(Task("TITLE1", "DESCRIPTION1", false, "id1"))
repository.saveTask(Task("TITLE2", "DESCRIPTION2", true, "id2"))
// GIVEN - On the home screen
val scenario = launchFragmentInContainer<TasksFragment>(Bundle(), R.style.AppTheme)
val navController = mock(NavController::class.java)
scenario.onFragment {
Navigation.setViewNavController(it.view!!, navController)
}
// WHEN - Click on the first list item
onView(withId(R.id.tasks_list))
.perform(RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
hasDescendant(withText("TITLE1")), click()))
// THEN - Verify that we navigate to the first detail screen
verify(navController).navigate(
TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment( "id1")
)
}- Ejecuta la prueba.
En resumen, para probar la navegación, puedes hacer lo siguiente:
- Usa Mockito para crear un objeto simulado
NavController. - Adjunta ese
NavControllersimulado al fragmento. - Verifica que se haya llamado a navigate con la acción y los parámetros correctos.
Paso 3: Opcional, write clickAddTaskButton_navigateToAddEditFragment
Para ver si puedes escribir una prueba de navegación por tu cuenta, intenta realizar esta tarea.
- Escribe la prueba
clickAddTaskButton_navigateToAddEditFragmentque verifica que, si haces clic en el botón de acción flotante +, se te redirecciona aAddEditTaskFragment.
La respuesta se encuentra a continuación.
TasksFragmentTest.kt
@Test
fun clickAddTaskButton_navigateToAddEditFragment() {
// GIVEN - On the home screen
val scenario = launchFragmentInContainer<TasksFragment>(Bundle(), R.style.AppTheme)
val navController = mock(NavController::class.java)
scenario.onFragment {
Navigation.setViewNavController(it.view!!, navController)
}
// WHEN - Click on the "+" button
onView(withId(R.id.add_task_fab)).perform(click())
// THEN - Verify that we navigate to the add screen
verify(navController).navigate(
TasksFragmentDirections.actionTasksFragmentToAddEditTaskFragment(
null, getApplicationContext<Context>().getString(R.string.add_task)
)
)
}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_2
También puedes descargar el repositorio como un archivo ZIP, descomprimirlo y abrirlo en Android Studio.
En este codelab, se explicó cómo configurar la inyección de dependencia manual, un localizador de servicios y cómo usar objetos simulados y falsos en tus apps para Android escritas en Kotlin. En particular:
- Lo que deseas probar y tu estrategia de pruebas determinan los tipos de pruebas que implementarás para tu app. Las pruebas de unidades son enfocadas y rápidas. Las pruebas de integración verifican la interacción entre las partes de tu programa. Las pruebas de extremo a extremo verifican las funciones, tienen la mayor fidelidad, a menudo se instrumentan y pueden tardar más en ejecutarse.
- La arquitectura de tu app influye en la dificultad de las pruebas.
- El TDD o desarrollo basado en pruebas es una estrategia en la que primero escribes las pruebas y, luego, creas la función para que las pruebas pasen.
- Para aislar partes de tu app y realizar pruebas, puedes usar dobles de prueba. Un doble de prueba es una versión de una clase creada específicamente para las pruebas. Por ejemplo, simulas obtener datos de una base de datos o de Internet.
- Usa la inserción de dependencias para reemplazar una clase real por una clase de prueba, por ejemplo, un repositorio o una capa de redes.
- Usa pruebas instrumentadas (
androidTest) para iniciar componentes de IU. - Cuando no puedes usar la inyección de dependencias del constructor, por ejemplo, para iniciar un fragmento, a menudo puedes usar un localizador de servicios. El patrón del localizador de servicios es una alternativa a la inyección de dependencias. Implica crear una clase singleton llamada "Service Locator", cuyo propósito es proporcionar dependencias, tanto para el código normal como para el de prueba.
Curso de Udacity:
Documentación para desarrolladores de Android:
- Guía de arquitectura de apps
runBlockingyrunBlockingTestFragmentScenario- Espresso
- Mockito
- JUnit4
- Biblioteca de prueba de AndroidX
- Biblioteca principal de pruebas de los componentes de arquitectura de AndroidX
- Conjuntos de orígenes
- Cómo realizar pruebas desde la línea de comandos
Videos:
Otro:
- Testing on the Toilet: Know Your Test Doubles (Pruebas en el baño: Conoce tus dobles de prueba)
- Muestra reactiva de Architecture Blueprints
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.




