Este codelab es parte del curso Aspectos avanzados de Android en Kotlin. Aprovecharás al máximo este curso si trabajas con los codelabs en secuencia, pero no es obligatorio. Todos los codelabs del curso se detallan en la página de destino de Codelabs avanzados de Android en Kotlin.
Introducción
En este segundo codelab de prueba, se trata de pruebas dobles: cuándo usarlas en Android y cómo implementarlas con la inserción de dependencias, el patrón del localizador de servicios y las bibliotecas. De este modo, aprenderás a escribir lo siguiente:
- Pruebas de la unidad de repositorio
- Pruebas de integración de Fragment y Viewmodel
- Pruebas de navegación de fragmentos
Conocimientos que ya deberías tener
Debes estar familiarizado con lo siguiente:
- Lenguaje de programación Kotlin
- Conceptos de prueba cubiertos en el primer codelab: Cómo escribir y ejecutar pruebas de unidades en Android con JUnit, Hamcrest, AndroidX, AndroidX y Robolectric, y cómo probar LiveData
- Las siguientes bibliotecas principales de Android Jetpack:
ViewModel
,LiveData
y el componente Navigation - Arquitectura de la aplicación, siguiendo el patrón de la Guía de arquitectura de apps y los codelabs de Aspectos básicos de Android
- Conceptos básicos sobre las corrutinas en Android
Qué aprenderás
- Cómo planificar una estrategia de prueba
- Cómo crear y usar dobles de prueba, es decir, simulaciones y simulaciones
- Cómo usar la inserción manual de dependencias en Android para pruebas de unidades y de integración
- Cómo aplicar el patrón del localizador de servicios
- Cómo probar repositorios, fragmentos, modelos de vistas y el componente Navigation
Usarás los siguientes conceptos relacionados con bibliotecas y código:
Actividades
- Escribir pruebas de unidades para un repositorio mediante doble inyección de prueba y de dependencias
- Escribir pruebas de unidades para un modelo de vista con una prueba doble e inyección de dependencia
- Escribir pruebas de integración para fragmentos y sus modelos de vistas con el framework de prueba de la IU de Espresso
- Escribir pruebas de navegación con Mockito y Espresso
En esta serie de codelabs, trabajarás con la app de notas de tareas pendientes. Esta te permite escribir tareas para completarlas y mostrarlas en una lista. Luego, puede marcarlos como completados o no, filtrarlos o borrarlos.
Esta app está escrita en Kotlin, tiene 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 arquitecturas.
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. Para ello, sigue las instrucciones que aparecen a continuación.
Paso 1: Ejecuta la app de muestra
Una vez que hayas descargado la app Pendientes, ábrela en Android Studio y ejecútala. Debería compilarse. Para explorar la app, haz lo siguiente:
- Crea una nueva tarea con el botón de acción flotante más. Ingresa un título y, luego, información adicional sobre la tarea. Guárdalo con el BAF de verificación verde.
- En la lista de tareas, haz clic en el título de la tarea que acabas de completar y observa la pantalla de detalles de esa tarea para ver el resto de la descripción.
- En la lista o en la pantalla de detalles, marque la casilla de verificación de esa tarea para establecer su estado en Completado.
- Regrese a la pantalla de tareas, abra el menú de filtro y filtre las tareas por estado Activo y Completado.
- Abre el panel lateral de navegación y haz clic en Estadísticas.
- Regresa a la pantalla de descripción general y, en el menú del panel lateral de navegación, selecciona Borrar tareas para borrar todas las tareas que tengan el estado Completadas.
Paso 2: Explora el código de la app de ejemplo
La app Pendientes se basa en las populares pruebas y arquitectura de Architecture Blueprints (que usan la versión de arquitectura reactiva de la muestra). La app sigue la arquitectura de una Guía de arquitectura de apps. Usa ViewModels con fragmentos, un repositorio y Room. Si conoces alguno de los ejemplos que se incluyen a continuación, esta app tiene una arquitectura similar:
- Codelab de Room con un componente View
- Codelabs de capacitación de Conceptos básicos de Kotlin para Android
- Codelabs avanzados de capacitación de Android
- Muestra de Sunflower de Android
- Curso de capacitación sobre cómo desarrollar apps para Android con Kotlin en Udacity
Es más importante que comprendas la arquitectura general de la app que una comprensión profunda de la lógica en una capa.
A continuación, se muestra el resumen de los paquetes que encontrarás:
Paquete: | |
| Pantalla de agregar o editar una tarea: Es el código de la capa de la IU para agregar o editar una tarea. |
| Capa de datos: Esta opción se aplica a la capa de datos de las tareas. Contiene la base de datos, la red y el código del repositorio. |
| Pantalla de estadísticas: Es el código de la capa de la IU para la pantalla de estadísticas. |
| Pantalla de detalles de la tarea: Es el código de la capa de la IU de una sola tarea. |
| Pantalla de tareas: Es el código de la capa de la IU para la lista de todas las tareas. |
| Clases de utilidad: Son clases compartidas que se usan en varias partes de la app, p.ej., para el diseño de actualización de deslizamiento que se usa en varias pantallas. |
Capa de datos (.data)
Esta app incluye una capa de red simulada, en el paquete remote, y una capa de base de datos, en el paquete local. Para simplificar, en este proyecto, la capa de herramientas de redes se simula con solo un HashMap
con un retraso, en lugar de realizar solicitudes de red reales.
DefaultTasksRepository
coordina o media la capa de red y la de la base de datos, y es lo que muestra los datos en la capa de la IU.
Capa de la IU ( .addingittask, .statistics, .taskdetail y .tasks)
Cada uno de los paquetes de la capa de la IU contiene un fragmento y un modelo de vista, junto con cualquier otra clase que se requiera para la IU (como un adaptador para la lista de tareas). El elemento TaskActivity
es la actividad que contiene todos los fragmentos.
Navegación
El componente de Navigation controla la navegación de la app. Se define en el archivo nav_graph.xml
. Se activa la navegación en los modelos de vista con la clase Event
; los modelos de vista también determinan qué argumentos pasar. Los fragmentos observan los Event
y realizan la navegación real entre pantallas.
En este codelab, aprenderás a probar repositorios, ver modelos y fragmentos mediante dobles de prueba y la inserción de dependencias. Antes de que te sumerjas en estas cuestiones, es importante que comprendas el razonamiento que te indicará qué escribirás y cómo escribirás las pruebas.
En esta sección, se abordan algunas de las prácticas recomendadas de las pruebas en general que se aplican a Android.
Pirámide de prueba
Cuando se piensa en una estrategia de prueba, existen tres aspectos relacionados:
- Alcance: ¿Cuánto del código toca 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? La velocidad de las pruebas puede variar desde milisegundos hasta varios minutos.
- Fidelidad: ¿Cómo es la prueba? Por ejemplo, si una parte del código que quieres probar es necesaria para realizar una solicitud de red, ¿el código de prueba realiza la solicitud de red o el resultado es falso? Si la prueba en realidad se comunica con la red, significa que tiene una mayor fidelidad. La desventaja es que la prueba podría tardar más tiempo en ejecutarse, podría generar errores si la red no funciona o podría ser costosa.
Existen compensaciones inherentes entre estos aspectos. Por ejemplo, la velocidad y la fidelidad son una compensación; cuanto más rápida sea la prueba, en general, menos fidelidad, y viceversa. Una forma común de dividir las pruebas automatizadas es en estas tres categorías:
- Pruebas de unidad: Son pruebas altamente enfocadas que se ejecutan en una sola clase, por lo general, un solo método en esa clase. Si una prueba de unidades falla, puedes saber con exactitud en qué parte del código se encuentra el problema. Son de 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ápidos para ejecutarse cada vez que cambias tu código. Por lo general, serán pruebas ejecutadas de manera local (en el conjunto de orígenes
test
). Ejemplo: Prueba de métodos únicos en modelos de vistas y repositorios. - Pruebas de integración: Prueban la interacción de varias clases para asegurarse de que se comporten como se espera 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. Las pruebas prueban un alcance mayor del código que las pruebas de unidades, pero están optimizadas para ejecutarse rápido en lugar de tener fidelidad total. Se pueden ejecutar de forma local o como pruebas de instrumentación, según la situación. Ejemplo: Prueba todas las funciones de un solo fragmento y par de modelos de vistas.
- Pruebas de extremo a extremo (E2e): Prueba una combinación de funciones que funcionan en conjunto. Evalúan grandes partes de la app, simulan el uso real de cerca y, por lo general, son lentos. Tienen la más alta fidelidad y indican que tu aplicación en realidad funciona como un todo. En general, estas pruebas serán instrumentadas (en el conjunto de orígenes
androidTest
).
Ejemplo: Inicia toda la app y prueba algunas funciones juntas.
La proporción sugerida de estas pruebas a menudo está representada por una pirámide, y la gran mayoría de ellas son pruebas de unidades.
Arquitectura y pruebas
La capacidad de probar tu app en todos los diferentes niveles de la pirámide de pruebas está inherentemente relacionada con la arquitectura de tu app. Por ejemplo, una aplicación con una arquitectura extremadamente incorrecta podría colocar toda su lógica dentro de un método. Podrías escribir una prueba de extremo a extremo para esto, ya que estas pruebas tienden a probar grandes partes de la app, pero ¿qué sucede con las pruebas de integración o de unidad? 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 permite probar cada pieza de forma aislada. La arquitectura es una forma de dividir y organizar tu código, lo que permite realizar pruebas de integraciones y unidades más fácilmente. La app PendienteS que probarás sigue una arquitectura en particular:
En esta lección, aprenderás a probar partes de la arquitectura anterior de forma independiente:
- Primero, prueba la unidad del repositorio.
- Luego, usarás un doble de prueba en el modelo de vista, que es necesario para la prueba de unidades y la prueba de integración en el modelo de vista.
- 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.
Las pruebas de extremo a extremo se abordarán en la próxima lección.
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 de una o varias clases específicas puede ser complicado. Veamos un ejemplo. Abre la clase data.source.DefaultTaskRepository
en el conjunto de orígenes main
. Este es el repositorio de la app y es la clase para la que escribirás pruebas de unidades.
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 decirlo es que LocalTaskDataSource
y RemoteTaskDataSource
son dependencias de DefaultTaskRepository
.
Por lo tanto, cada método de DefaultTaskRepository
llama a métodos de clases de fuente de datos que, a su vez, llaman a métodos de 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"a tu repositorio. Este método incluye leer desde una base de datos SQLite y realizar llamadas de red (la llamada a updateTasksFromRemoteDataSource
). Esto implica mucho más código que solo el código del repositorio.
Estos son algunos de los motivos más específicos por los que resulta difícil probar el repositorio:
- Debes lidiar con la creación y la administración de una base de datos para realizar incluso las pruebas más simples de este repositorio. Esto genera preguntas como: si debería ser una prueba local o instrumentada, y si debes usar la prueba de AndroidX para obtener un entorno de Android simulado.
- Algunas partes del código, como el código de red, pueden tardar mucho tiempo en ejecutarse o, en ocasiones, fallar, lo que genera pruebas inestables y prolongadas.
- Es posible que las pruebas pierdan la capacidad de diagnosticar qué código presenta errores durante una prueba fallida. Las pruebas pueden comenzar a probar el código que no es del repositorio. Por ejemplo, las previstas pruebas de unidades 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 para esto es que, cuando pruebes el repositorio, no uses el código real de red o de base de datos, sino el de la prueba doble. Un doble de prueba es una versión de una clase creada específicamente para realizar pruebas. Su objetivo es reemplazar la versión real de una clase en las pruebas. Es similar a cómo un doble de acrobacias es un actor que se especializa en acrobacias y reemplaza al actor por acciones peligrosas.
Estos son algunos tipos de dobles de prueba:
Falso | Un doble de prueba que tiene una implementación de la clase, pero se implementa de un modo que hace que sea bueno para las pruebas, pero que no es adecuado para la producción. |
Simulación | Un doble de prueba que realiza un seguimiento de los métodos que se llamaron. Luego, aprueba o reprueba una prueba según si se llamaron correctamente los métodos. |
Stub | Un doble de prueba que no incluye lógica y solo muestra lo que programaste para mostrar. Por ejemplo, se puede programar un objeto |
De prueba | Un doble de prueba que se pasa sin usar, por ejemplo, si solo tienes que proporcionarlo como parámetro. Si tienes un |
Espionaje | 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 dobles de prueba, consulta Pruebas en los inodoros: Conoce los dobles de prueba.
Los dobles de prueba más comunes que se usan en Android son Fakes y Mocks.
En esta tarea, crearás una prueba FakeDataSource
doble para la unidad DefaultTasksRepository
separada 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 LocalDataSource
y RemoteDataSource
.
- En el conjunto de orígenes test, haz clic con el botón derecho en New -> Package.
- Crea un paquete data con un paquete source dentro.
- Crea una clase nueva llamada
FakeDataSource
en el paquete data/source.
Paso 2: Implementa la interfaz TasksDataSource
Para poder usar tu nueva clase FakeDataSource
como doble de prueba, debe ser capaz de 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
FakeDataSource
implementeTasksDataSource
:
class FakeDataSource : TasksDataSource {
}
Android Studio anunciará que no has implementado los métodos obligatorios para TasksDataSource
.
- Usa el menú de corrección rápida y selecciona Implementar miembros.
- Selecciona todos los métodos y presiona OK.
Paso 3: Implementa el método getTasks en FakeDataSource
FakeDataSource
es un tipo de prueba doble específico llamado falso. Un falso es un doble de prueba que tiene una implementación de la clase, pero se implementa de un modo que hace que sea bueno para las pruebas, pero que no es adecuado para la producción. La implementación significa que la clase producirá resultados realistas a partir de entradas determinadas.
Por ejemplo, tu fuente de datos falsa no se conectará a la red ni guardará nada en una base de datos, solo se usará una lista en la memoria. Esto funcionará como se espera: con esos métodos para obtener o guardar tareas mostrará los resultados esperados, pero nunca podrás usar esta implementación en producción porque no se guarda en el servidor ni en una base de datos.
Un FakeDataSource
- te permite probar el código en
DefaultTasksRepository
sin necesidad de depender de una red o base de datos reales. - proporciona una implementación "suficiente" para pruebas.
- Cambia el constructor
FakeDataSource
a fin de crear unvar
llamadotasks
que sea unaMutableList<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 son falsificaciones (una respuesta de base de datos o servidor). Por ahora, el objetivo es probar el método repositoriogetTasks
. Esto llama a los métodos de la fuente de datosgetTasks
, deleteAllTasks
y saveTask
.
Escribe una versión falsa de estos métodos:
- Escribe
getTasks
: sitasks
no esnull
, muestra un resultadoSuccess
. Sitasks
esnull
, muestra un resultadoError
. - Escribe
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)
}
Si es necesario, estas son las sentencias de importación:
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
Esto funciona de manera similar a las fuentes de datos locales y remotas.
En este paso, usarás una técnica llamada inyección manual de dependencias, de modo 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 usarlo 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 estás creando y asignando taskLocalDataSource
y tasksRemoteDataSource
dentro de DefaultTasksRepository
, básicamente están hard-coded. No hay forma de cambiar el doble de prueba.
Lo que se debe hacer en su lugar es proporcionar estas fuentes de datos a la clase, en lugar de codificarlas. Proporcionar dependencias se conoce como inserción de dependencias. Existen diferentes maneras de proporcionar dependencias y, por lo tanto, diferentes tipos de inyección de dependencias.
La inyección de dependencias de 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
DefaultTaskRepository
de unaApplication
a las fuentes de datos y al despachador de corrutinas (que también deberás intercambiar para tus pruebas). Esto se describe con más detalle en la tercera sección de lecciones 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 crear las dependencias. - Borrar también las variables de instancia anteriores. Los definirás 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
getRepository
para 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 inyección de dependencia de constructor.
Paso 2: Usa tu FakeDataSource en tus pruebas
Ahora que tu código usa la inserción de dependencias de constructor, puedes usar tu fuente de datos falsos para probar tu DefaultTasksRepository
.
- Haz clic con el botón derecho en el nombre de la clase
DefaultTasksRepository
, selecciona Generar y, luego, Probar. - Sigue las indicaciones para crear
DefaultTasksRepositoryTest
en el conjunto de orígenes de prueba. - En la parte superior de la nueva clase
DefaultTasksRepositoryTest
, agrega las siguientes variables de miembro para representar los datos de tus fuentes de datos falsas.
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 de miembro
FakeDataSource
, una para cada fuente de datos para tu repositorio y una variable para elDefaultTasksRepository
que probarás.
DefaultTasksRepositoryTest.kt
private lateinit var tasksRemoteDataSource: FakeDataSource
private lateinit var tasksLocalDataSource: FakeDataSource
// Class under test
private lateinit var tasksRepository: DefaultTasksRepository
Crea un método para configurar e inicializar un DefaultTasksRepository
que se pueda probar. Este DefaultTasksRepository
usará el doble de prueba, FakeDataSource
.
- Crea un método llamado
createRepository
y anótalo con@Before
. - Crea instancias de tus fuentes de datos falsas con las listas
remoteTasks
ylocalTasks
. - Crea una instancia de tu
tasksRepository
con 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 Task DefaultTasksRepository getTasks()
Es hora de escribir una prueba de DefaultTasksRepository
.
- Escribe una prueba para el método
getTasks
del repositorio. Verifica que, cuando llames agetTasks
contrue
(que significa que debe volver a cargarse desde la fuente de datos remota), esta mostrará 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á un error cuando llame al getTasks:
Paso 4: Agrega runBlockingTest
Se espera el error de la 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, tendrás que agregar algunas dependencias de Gradle a fin de controlar el inicio de corrutinas en tus pruebas.
- Agrega las dependencias necesarias para probar corrutinas en el conjunto de orígenes de pruebas 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 las 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, ejecuta este bloque de código en un contexto de corrutina especial que se ejecuta de forma síncrona e inmediata, lo que significa que las acciones se realizarán en un orden determinista. En esencia, tus corrutinas se ejecutan como no corrutinas, por lo que está diseñado para probar el código.
Usa runBlockingTest
en tus clases de prueba cuando llames a una función suspend
. En el siguiente codelab de esta serie, obtendrás más información sobre cómo funciona runBlockingTest
y cómo probar corrutinas.
- Agrega el elemento
@ExperimentalCoroutinesApi
arriba de la clase. Esto expresa que sabes que estás usando una API de corrutinas experimental (runBlockingTest
) en la clase. Si no lo agregas, recibirás una advertencia. - Vuelve a tu
DefaultTasksRepositoryTest
y agregarunBlockingTest
para que abarque toda la prueba como un bloque de código.
Esta prueba final se parece al 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 la prueba nueva
getTasks_requestsAllTasksFromRemoteDataSource
y confirma que funcione, y ya no habrá errores.
Ya viste cómo realizar la prueba de unidades de un repositorio. En los siguientes pasos, volverás a usar la inserción de dependencias y crearás otra prueba doble, esta vez para mostrar cómo escribir pruebas de integración y de unidades para tus modelos de vista.
Las pruebas de unidades solo deben probar la clase o el método que te interesa. Esto se conoce como prueba en aislamiento, en el que se aísla claramente el "bloque" y solo se prueba el código que forma parte de esa unidad.
Por lo tanto, TasksViewModelTest
solo debe probar código TasksViewModel
; no debe probarlo en las clases de base de datos, red ni repositorio. Por lo tanto, para tus modelos de vista, tal como lo hiciste con tu repositorio, crearás un repositorio falso y aplicarás la inserción de dependencias para usarlo en tus pruebas.
En esta tarea, aplicará la inyección de dependencias para ver los modelos.
Paso 1: Crea una interfaz de TasksRepository
El primer paso para usar la inserción de dependencias de constructor es crear una interfaz común compartida entre la clase falsa y la real.
¿Cómo funciona esto en la práctica? Observa TasksRemoteDataSource
, TasksLocalDataSource
y FakeDataSource
. Observa que todos comparten la misma interfaz: TasksDataSource
. Esto te permite decir 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 con las fuentes de datos. Debe incluir todos los métodos públicos (plataforma de API pública) de DefaultTasksRepository
.
- Abre
DefaultTasksRepository
y haz clic con el botón derecho en el nombre de la clase. Luego, selecciona Refactor -> Extract -> Interface.
- Selecciona Extraer para separar el archivo.
- En la ventana Extract Interface, cambia el nombre de la interfaz a
TasksRepository
. - En la sección Miembros para formar una interfaz, verifique todos los miembros, excepto los dos miembros complementarios y los métodos privados.
- Haz clic en Refactor. La nueva interfaz de
TasksRepository
debería aparecer en el paquete de datos/fuentes.
Y DefaultTasksRepository
ahora implementa TasksRepository
.
- Ejecuta la app (no las pruebas) para asegurarte de que todo esté en funcionamiento.
Paso 2: Crea FakeTestRepository
Ahora que tienes la interfaz, puedes crear la prueba DefaultTaskRepository
doble.
- En el conjunto de orígenes test , en data/source, crea el archivo y la clase
FakeTestRepository.kt
de Kotlin y extiéndelos desde la interfazTasksRepository
.
FakeTestRepository.kt
class FakeTestRepository : TasksRepository {
}
Se le indicará que debe implementar los métodos de la interfaz.
- Coloque el cursor sobre el error hasta que aparezca el menú de sugerencias y, luego, haga clic y seleccione Implement members.
- Selecciona todos los métodos y presiona OK.
Paso 3: Implementa métodos FakeTestRepository
Ahora tienes una clase FakeTestRepository
con métodos no implementados. De manera similar a como implementaste el FakeDataSource
, el FakeTestRepository
estará respaldado por 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 FakeDataSource
ni otros elementos similares, solo debe mostrar resultados falsos realistas a partir de entradas determinadas. Usarás una LinkedHashMap
para almacenar la lista de tareas y un MutableLiveData
para las tareas observables.
- En
FakeTestRepository
, agrega una variableLinkedHashMap
que represente la lista actual de tareas y unMutableLiveData
para las 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 eltasksServiceData
y convertirlo en una lista mediantetasksServiceData.values.toList()
y, luego, mostrarlo como un resultado deSuccess
.refreshTasks
: Actualiza el valor deobservableTasks
para que sea el que muestragetTasks()
.observeTasks
: Crea una corrutina medianterunBlocking
y ejecutarefreshTasks
y, luego, muestraobservableTasks
.
A continuación, se incluye 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 realizar pruebas en addTasks
Cuando realices pruebas, es mejor tener algunos Tasks
en tu repositorio. Puedes llamar a saveTask
varias veces, pero para facilitar este proceso, agrega un método de ayuda específico para pruebas que te permita agregar tareas.
- Agrega el método
addTasks
, que toma unvararg
de tareas, agrega cada una a laHashMap
y, 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 probar con algunos de los métodos clave implementados. Luego, úsalo en tus pruebas.
En esta tarea, usarás una clase falsa dentro de una ViewModel
. Usa la inserción de dependencias del constructor para agregar las dos fuentes de datos mediante la inserción de dependencias del constructor agregando una variable TasksRepository
al constructor TasksViewModel
.
Este proceso es un poco diferente con los modelos de vistas porque 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 construcción del modelo de vista, deberás agregar y usar un objeto ViewModelProvider.Factory
. Si no conoces ViewModelProvider.Factory
, puedes obtener más información aquí.
Paso 1: Cómo crear y usar ViewModelFactory en TasksViewModel
Primero, debes actualizar las clases y realizar las pruebas relacionadas con la pantalla de Tasks
.
- Abre
TasksViewModel
. - Cambia el constructor de
TasksViewModel
de modo que tomeTasksRepository
en 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
}
Dado que cambiaste el constructor, ahora debes usar una fábrica para construir TasksViewModel
. Coloca la clase de fábrica en el mismo archivo que el TasksViewModel
, pero también puedes colocarlo en su propio archivo.
- En la parte inferior del archivo
TasksViewModel
, fuera de la clase, agrega unTasksViewModelFactory
que reciba unTasksRepository
simple.
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 las ViewModel
. Ahora que tienes la fábrica, úsala donde crees tu modelo de vista.
- Actualiza
TasksFragment
para usar la configuración de 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 correctamente.
Paso 2: Usa FakeTestRepository dentro de TasksViewModelTest
Ahora, en lugar de usar el repositorio real en tus pruebas de modelo de vista, puedes usar el repositorio falso.
- Abre
TasksViewModelTest
. - Agrega una propiedad
FakeTestRepository
aTasksViewModelTest
.
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
setupViewModel
para crear unFakeTestRepository
con tres tareas y, luego, crea eltasksViewModel
con 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 de AndroidX Test
ApplicationProvider.getApplicationContext
, también puedes quitar la anotación@RunWith(AndroidJUnit4::class)
. - Ejecuta las pruebas y asegúrate de que todas funcionen.
Mediante la inserción de dependencias del constructor, ahora quitaste el DefaultTasksRepository
como dependencia y lo reemplazaste por tu FakeTestRepository
en las pruebas.
Paso 3: Actualiza también Fragment y ViewModel de TaskDetails
Realiza los mismos cambios de TaskDetailFragment
y TaskDetailViewModel
. Esto preparará el código para cuando escribas pruebas TaskDetail
a continuación.
- Abre
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
TasksFragment
para usar la configuración de 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 FakeTestRepository
en lugar del repositorio real en TasksFragment
y TasksDetailFragment
.
A continuación, escribirás pruebas de integración para probar las interacciones de fragmentos y modelos de vistas. Descubrirás si el código de tu modelo de vista actualiza correctamente tu IU. Para hacer esto, debes usar
- 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 como se espera cuando se usan juntas. Estas pruebas se pueden ejecutar de forma local (conjunto de orígenes test
) o como pruebas de instrumentación (conjunto de orígenes androidTest
).
En tu caso, tomarás cada fragmento y escribirás pruebas de integración para el fragmento y el modelo de vista a fin de probar las características 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"
Entre estas dependencias, se incluyen las siguientes:
junit:junit
: JUnit, que es necesario para escribir declaraciones de prueba básicas.androidx.test:core
: Biblioteca de prueba principal de AndroidXkotlinx-coroutines-test
: La 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 el conjunto de orígenes de androidTest
, usa androidTestImplementation
para agregarlas como dependencias.
Paso 2: Crea una clase TaskDetailFragmentTest
TaskDetailFragment
muestra información sobre una sola tarea.
Comenzarás escribiendo una prueba de fragmentos para TaskDetailFragment
, ya que tiene una funcionalidad bastante básica en comparación con los otros fragmentos.
- Abre
taskdetail.TaskDetailFragment
. - Genera una prueba para
TaskDetailFragment
, como hiciste antes. Acepta las opciones predeterminadas y colócala 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 medio (en comparación con las pruebas de unidades de@SmallTest
y@LargeTest
de extremo a extremo). Esto te ayudará a elegir el tamaño de prueba que quieras ejecutar.@RunWith(AndroidJUnit4::class)
: Se usa en cualquier clase mediante 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 prueba de AndroidX que envuelve un fragmento y te da control directo sobre el ciclo de vida del fragmento para probarlo. Si quieres escribir pruebas para fragmentos, crea un FragmentScenario
para el fragmento que estás probando (TaskDetailFragment
).
- Copia esta prueba a
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:
- Crea una tarea.
- Crea un
Bundle
, que representa los argumentos del fragmento para la tarea que se pasa al fragmento. - La función
launchFragmentInContainer
crea un elementoFragmentScenario
con este paquete y un tema.
Esta aún no es una prueba terminada porque no se declara nada. Por ahora, ejecuta la prueba y observa lo que 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, debido a que se trata de una prueba instrumentada, esta 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 menús asociados con la actividad: es solo el fragmento.
Por último, observa con atención y observa que el fragmento dice "No hay datos", ya que no carga los datos de la tarea correctamente.
Tu prueba debe cargar el TaskDetailFragment
(que realizaste) 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 manera de reemplazar el repositorio real por uno falso para tu fragmento. Lo harás a continuación.
En esta tarea, proporcionarás tu repositorio falso a tu fragmento usando un ServiceLocator
. Esto te permitirá escribir el fragmento y ver las pruebas de integración del modelo.
Aquí no puedes usar la inserción de dependencias del constructor, como lo hiciste antes, cuando necesitas proporcionar una dependencia al modelo de vista o al repositorio. La inyección de dependencia de constructor requiere que construyas la clase. Los fragmentos y las actividades son ejemplos de clases que no construyes y generalmente no tienes acceso al constructor.
Como no construyes el fragmento, no puedes usar la inyección de dependencia de constructor para intercambiar la prueba de repositorio doble (FakeTestRepository
) por el fragmento. En su lugar, usa el patrón Localizador de servicios. El patrón del localizador de servicios es una alternativa a la inserción de dependencias. Implica crear una clase singleton llamada &localizador de servicio, cuyo propósito es proporcionar dependencias tanto para el código normal como para el código de prueba. En el código normal de la app (el conjunto de orígenes main
), todas estas dependencias son las normales de la app. Para las pruebas, modifica el localizador de servicios a fin de proporcionar versiones dobles de prueba de las dependencias.
No uso el localizador de servicios | Usa un localizador de servicios |
En esta app de codelab, haz lo siguiente:
- Crea una clase de localizador de servicios que pueda construir y almacenar un repositorio. De forma predeterminada, construye un repositorio "normal".
- Refactoriza tu código para que, cuando necesites un repositorio, use el localizador de servicios.
- En la clase de prueba, llama a un método en el localizador de servicios que intercambie el repositorio de "normal" con el doble de prueba.
Paso 1: Crea el ServiceLocator
Hagamos una clase ServiceLocator
. Permanecerá en el conjunto de orígenes principal con el resto del código de la app, ya que el código de la app principal lo usa.
Nota: La 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 principal de orígenes.
- Define un
object
llamadoServiceLocator
. - Crea variables de instancia
database
yrepository
y establece ambas ennull
. - Anota el repositorio con
@Volatile
porque podría usarse en varios subprocesos (@Volatile
se 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 tu ServiceLocator
debe hacer es saber cómo mostrar un TasksRepository
. Mostrará un DefaultTasksRepository
preexistente o, si lo necesita, mostrará un DefaultTasksRepository
nuevo.
Define las siguientes funciones:
provideTasksRepository
: Proporciona un repositorio existente o crea uno nuevo. Este método debe sersynchronized
enthis
para evitar que se produzcan dos instancias de repositorio en situaciones con varios subprocesos en ejecución.createTasksRepository
: Código para crear un repositorio nuevo. Llamará acreateTaskLocalDataSource
y creará un nuevoTasksRemoteDataSource
.createTaskLocalDataSource
: Código para crear una nueva fuente de datos local. Se llamará acreateDataBase
.createDataBase
: Código para crear una base de datos nueva.
El código completo se encuentra a continuación.
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: Usa ServiceLocator en la aplicación
Vas a realizar un cambio en el código principal de tu aplicación (no en las pruebas) para que crees el repositorio en un solo lugar: tu ServiceLocator
.
Es importante que solo realices una instancia de la clase de repositorio. Para garantizar esto, usarás el localizador de servicios en mi clase Application.
- En el nivel superior de la jerarquía de tu paquete, abre
TodoApplication
y crea unval
para 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
DefaultTasksRepository
y 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, donde estés usando getRepository
, usa el taskRepository
de la aplicación. Esto garantiza que, en lugar de crear el repositorio directamente, obtendrás el repositorio que proporcionó el ServiceLocator
.
- Abre
TaskDetailFragement
y busca la llamada agetRepository
en la parte superior de la clase. - Reemplaza esta llamada por una llamada 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
StatisticsViewModel
yAddEditTaskViewModel
, 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 la aplicación (no la prueba).
Como solo refactorizaste, la app debería ejecutarse de la misma manera sin problemas.
Paso 3: Crea FakeAndroidTestRepository
Ya tienes una FakeTestRepository
en el conjunto de orígenes de pruebas. No puedes compartir clases de prueba entre los conjuntos de orígenes test
y androidTest
de forma predeterminada. Por lo tanto, debes hacer una clase FakeTestRepository
duplicada en el conjunto de orígenes androidTest
y llamarla FakeAndroidTestRepository
.
- Haz clic con el botón derecho en el conjunto de orígenes
androidTest
y crea un paquete de data. Vuelve a hacer clic con el botón derecho y crea un paquete source. - Crea una clase nueva en este paquete de origen, 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 pruebas
De acuerdo, es hora de usar ServiceLocator
para cambiar a modo de prueba y se duplica cuando se realizan pruebas. Para ello, debes agregar código a tu código ServiceLocator
.
- Abre
ServiceLocator.kt
. - Marca el establecedor de
tasksRepository
como@VisibleForTesting
. Esta anotación es una forma de expresar que el motivo por el que el establecedor es público se debe a que se realizaron pruebas.
ServiceLocator.kt
@Volatile
var tasksRepository: TasksRepository? = null
@VisibleForTesting set
Ya sea que ejecutes la prueba sola o en un grupo de pruebas, estas deberían ejecutarse exactamente de la misma manera. Esto significa que las pruebas no deben tener ningún comportamiento que dependa de las demás (lo que implica evitar el intercambio de objetos entre las pruebas).
Dado que ServiceLocator
es un singleton, tiene la posibilidad de que se comparta accidentalmente entre pruebas. Para evitar que esto suceda, crea un método que restablezca correctamente el estado ServiceLocator
entre pruebas.
- Agrega una variable de instancia llamada
lock
con el valorAny
.
ServiceLocator.kt
private val lock = Any()
- Agrega un método específico de prueba llamado
resetRepository
que borre la base de datos y configure el repositorio y la base de datos en nulo.
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
.
- Abre
TaskDetailFragmentTest
. - Declara una variable
lateinit TasksRepository
. - Agrega un método de configuración y eliminación para establecer
FakeAndroidTestRepository
antes de cada prueba y limpiarla después de cada una.
TaskDetailFragmentTest.kt;
private lateinit var repository: TasksRepository
@Before
fun initRepository() {
repository = FakeAndroidTestRepository()
ServiceLocator.tasksRepository = repository
}
@After
fun cleanupDb() = runBlockingTest {
ServiceLocator.resetRepository()
}
- Une el cuerpo de la función
activeTaskDetails_DisplayedInUi()
enrunBlockingTest
. - Guarda
activeTask
en 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, excepto esta vez, ya que configuraste correctamente el repositorio. Ahora, se muestra la información de la tarea.
En este paso, usarás la biblioteca de pruebas de la IU de Espresso para completar tu primera prueba de integración. Estructuraste tu código a fin de que puedas agregar pruebas con aserciones para tu IU. Para ello, usarás la biblioteca de pruebas de Espresso.
Espresso lo ayuda a hacer lo siguiente:
- Interactuar con las vistas, como hacer clic en botones, deslizar una barra o desplazarse hacia abajo en una pantalla
- Confirma que algunas vistas están en pantalla o en un estado determinado (como contener texto en particular o que una casilla de verificación está marcada, etcétera).
Paso 1: Ten en cuenta la dependencia de Gradle
Ya tendrás la dependencia principal de Espresso, ya que se incluye en 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 proyecto de Android nuevo. Contiene el código de prueba básico para la mayoría de las vistas y acciones relacionadas.
Paso 2: Cómo desactivar las animaciones
Las pruebas de Espresso se ejecutan en un dispositivo real y, por lo tanto, son de instrumentación por naturaleza. Un problema que se presenta son las animaciones: Si una animación se retrasa y tratas de probar si una vista está en la pantalla, pero todavía se está animando, Espresso puede fallar una prueba accidentalmente. Esto puede hacer que las pruebas de Espresso sean inestables.
Para las pruebas de IU de Espresso, se recomienda desactivar las animaciones (también se puede ejecutar tu prueba más rápido):
- En el dispositivo de prueba, ve a Configuración > Opciones para desarrolladores.
- Inhabilita estas tres configuraciones: Escala de animación de ventana, Escala de animación de transición y Escala de duración de animador.
Paso 3: Mira una prueba de Espresso
Antes de escribir una prueba Espresso, observa algunos códigos Espresso.
onView(withId(R.id.task_detail_complete_checkbox)).perform(click()).check(matches(isChecked()))
Lo que hace esta sentencia es encontrar 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
onView
onView
es un ejemplo de un método Espresso estático que inicia una declaració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. Puedes encontrar otros comparadores de vistas en la documentación.
3. ViewAction.
perform(click())
El método perform
que toma un elemento ViewAction
Un objeto ViewAction
es algo que se puede hacer a la vista, por ejemplo, cuando se hace clic en la vista.
check(matches(isChecked()))
check
que toma un ViewAssertion
ViewAssertion
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 debes llamar a perform
y check
en una sentencia Espresso. Puedes tener sentencias que solo hagan una aserción con check
o solo hagas una ViewAction
con perform
.
- Abre
TaskDetailFragmentTest.kt
. - Actualiza la prueba de
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())))
}
Si es necesario, estas son las sentencias de importación:
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 esté después del comentario
// THEN
usará Espresso. Examina la estructura de prueba y el uso dewithId
, y realiza comprobaciones para ver cómo debería verse la página de detalles. - Ejecuta la prueba y confirma que se apruebe.
Paso 4: Opcional: escribe tu propia prueba Espresso
Ahora, escribe una prueba tú mismo.
- Crea una prueba nueva llamada
completedTaskDetails_DisplayedInUi
y copia este código base.
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
}
- Si observa la prueba anterior, complete esta.
- Ejecuta y confirma que se haya aprobado la prueba.
El completedTaskDetails_DisplayedInUi
finalizado debería verse de la siguiente manera:
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 Navigation con un tipo de prueba diferente llamado prueba simulada y la biblioteca de pruebas Mockito.
En este codelab, usaste un doble de prueba denominado falso. Los falsos son uno de los muchos tipos de dobles de prueba. ¿Qué tipo de prueba doble debes usar para probar el componente de Navigation?
Piensa en cómo se realiza la navegación. Imagina presionar una de las tareas de TasksFragment
para navegar a una pantalla de detalles de la tarea.
Este es el código de TasksFragment
que navega a la pantalla de detalles de una 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 declaración de aserción, no hay una manera directa de probar si navegaste a TaskDetailFragment
. La navegación 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
.
Puedes confirmar que se llamó al método navigate
con el parámetro de acción correcto. Esto es exactamente lo que hace una doble prueba simultánea: verifica si se llamó a métodos específicos.
Mockito es un marco de trabajo para hacer dobles de prueba. Si bien la palabra simulada se usa en la API y el nombre, no solo para simular. También puede crear stubs y espías.
Usarás Mockito para crear un NavigationController
ficticio que pueda afirmar 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
: Esta es la dependencia de Mockito.dexmaker-mockito
: Esta biblioteca es obligatoria para usar Mockito en un proyecto de Android. Mockito necesita generar clases en el tiempo de ejecución. En Android, esto se hace con un código de bytes dex, por lo que esta biblioteca le permite a Mockito generar objetos durante el tiempo de ejecución en Android.androidx.test.espresso:espresso-contrib
: Esta biblioteca consta de contribuciones externas (por eso se llama nombre) que contienen código de prueba para vistas más avanzadas, comoDatePicker
yRecyclerView
. También contiene las verificaciones de accesibilidad y la claseCountingIdlingResource
, que se explicará más adelante.
Paso 2: Crea TasksFragmentTest
- Abre
TasksFragment
. - Haz clic con el botón derecho en el nombre de la clase
TasksFragment
, selecciona Generar y, luego, Probar. Crea una prueba en el conjunto de orígenes androidTest. - Copia este código en el archivo
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 TaskDetailFragmentTest
que escribiste. Configura y quita un elemento FakeAndroidTestRepository
. Agrega una prueba de navegación para comprobar que, cuando hagas clic en una tarea de la lista correspondiente, 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
mock
de Mockito para crear un modelo.
TasksFragmentTest.kt
val navController = mock(NavController::class.java)
Para simular en Mockito, pasa la clase que deseas simular.
A continuación, debes asociar tu NavController
con el fragmento. onFragment
te permite llamar a métodos en el propio fragmento.
- Haz que tu nuevo modelo sea el
NavController
del fragmento.
scenario.onFragment {
Navigation.setViewNavController(it.view!!, navController)
}
- Agrega el código para hacer clic en el elemento de
RecyclerView
que tiene el texto &TITLE1;TITLE1"
// WHEN - Click on the first list item
onView(withId(R.id.tasks_list))
.perform(RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
hasDescendant(withText("TITLE1")), click()))
RecyclerViewActions
es parte de la biblioteca de espresso-contrib
y te permite realizar acciones de Espresso en una RecyclerView.
- Verifica que se haya llamado a
navigate
con 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 el navController
simulado llamado método específico (navigate
) con un parámetro (actionTasksFragmentToTaskDetailFragment
con el ID de "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 modelo de
NavController
. - Conecta ese
NavController
ficticio al fragmento. - Verifica que se haya llamado a la navegación con la acción y los parámetros correctos.
Paso 3: Opcional: escribe clickAddTaskButton_NavigateToAddEditFragment
Para ver si puedes escribir una prueba de navegación tú mismo, prueba esta tarea.
- Escribe el
clickAddTaskButton_navigateToAddEditFragment
de prueba que verifica que, si haces clic en el BAF + +, navegues aAddEditTaskFragment
.
La respuesta está 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 diferencia entre el código que comenzaste y el código final.
Para descargar el código del codelab terminado, puedes usar el siguiente comando de git:
$ git clone https://github.com/googlecodelabs/android-testing.git $ cd android-testing $ git checkout end_codelab_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 inserción manual de dependencias, un localizador de servicios y cómo usar emulaciones y simulaciones en tus apps de Kotlin para Android. En particular, considera lo siguiente:
- Lo que deseas probar y tu estrategia de prueba determinan los tipos de pruebas que implementarás para tu app. Las pruebas de unidades se enfocan y son rápidas. Las pruebas de integración verifican la interacción entre las partes del programa. Las pruebas de extremo a extremo verifican las funciones, tienen la mayor fidelidad, suelen estar instrumentadas y pueden tardar más en ejecutarse.
- La arquitectura de tu app influye en qué tan difícil es probarla.
- El desarrollo basado en pruebas o TDD es una estrategia en la que primero escribes las pruebas y, luego, creas la función para aprobarlas.
- Para aislar partes de la app a fin de realizar pruebas, puedes usar dobles de prueba. Un doble de prueba es una versión de una clase creada específicamente para realizar pruebas. Por ejemplo, falsificas la obtención de datos de una base de datos o 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 herramientas de redes.
- Usa las pruebas integradas (
androidTest
) para iniciar componentes de IU. - Cuando no puedes usar la inyección de dependencia de 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 inserción de dependencias. Implica crear una clase singleton llamada &localizador de servicio, cuyo propósito es proporcionar dependencias tanto para el código normal como para el código de prueba.
Curso de Udacity:
Documentación para desarrolladores de Android:
- Guía de arquitectura de apps
runBlocking
yrunBlockingTest
FragmentScenario
- Espresso
- Mockito
- JUnit4;
- Biblioteca de pruebas de AndroidX
- Biblioteca principal de pruebas de componentes de la arquitectura de AndroidX
- Conjuntos de orígenes
- 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 Codelabs avanzados de Android en Kotlin.