Introducción a las pruebas dobles y la inserción de dependencias

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:

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:

Download Zip

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

$ git clone https://github.com/googlecodelabs/android-testing.git
$ cd android-testing
$ git checkout 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:

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

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

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

.addedittask

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

.data

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

.statistics

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

.taskdetail

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

.tasks

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

.util

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

Capa de datos (.data)

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

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

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

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

Navegación

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

En 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:

  1. Primero, prueba la unidad del repositorio.
  2. 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.
  3. A continuación, aprenderás a escribir pruebas de integración para fragmentos y sus modelos de vista.
  4. 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 StubTaskRepository para que muestre ciertas combinaciones de tareas de getTasks.

De prueba

Un doble de prueba que se pasa sin usar, por ejemplo, si solo tienes que proporcionarlo como parámetro. Si tienes un NoOpTaskRepository, solo implementará TaskRepository sin ningún código en ninguno de los métodos.

Espionaje

Un doble de prueba que también realiza un seguimiento de cierta información adicional; por ejemplo, si creaste un SpyTaskRepository, podría realizar un seguimiento de la cantidad de veces que se llamó al método addTask.

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.

  1. En el conjunto de orígenes test, haz clic con el botón derecho en New -> Package.

  1. Crea un paquete data con un paquete source dentro.
  2. 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.

  1. 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 { ... }
  1. Haz que FakeDataSource implemente TasksDataSource:
class FakeDataSource : TasksDataSource {

}

Android Studio anunciará que no has implementado los métodos obligatorios para TasksDataSource.

  1. Usa el menú de corrección rápida y selecciona Implementar miembros.


  1. 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.
  1. Cambia el constructor FakeDataSource a fin de crear un var llamado tasks que sea una MutableList<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:

  1. Escribe getTasks: si tasks no es null, muestra un resultado Success. Si tasks es null, muestra un resultado Error.
  2. Escribe deleteAllTasks: Borra la lista de tareas mutables.
  3. 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

  1. Cambia el constructor de DefaultTaskRepository de una Application 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 }
  1. Como pasaste las dependencias, quita el método init. Ya no es necesario crear las dependencias.
  2. 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
  1. 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.

  1. Haz clic con el botón derecho en el nombre de la clase DefaultTasksRepository, selecciona Generar y, luego, Probar.
  2. Sigue las indicaciones para crear DefaultTasksRepositoryTest en el conjunto de orígenes de prueba.
  3. 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 }
  1. Crea tres variables de miembro FakeDataSource, una para cada fuente de datos para tu repositorio y una variable para el DefaultTasksRepository 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.

  1. Crea un método llamado createRepository y anótalo con @Before.
  2. Crea instancias de tus fuentes de datos falsas con las listas remoteTasks y localTasks.
  3. Crea una instancia de tu tasksRepository con las dos fuentes de datos falsas que acabas de crear y Dispatchers.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.

  1. Escribe una prueba para el método getTasks del repositorio. Verifica que, cuando llames a getTasks con true (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.

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

  1. 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.
  2. Vuelve a tu DefaultTasksRepositoryTest y agrega runBlockingTest 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))
    }

}
  1. 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.

  1. Abre DefaultTasksRepository y haz clic con el botón derecho en el nombre de la clase. Luego, selecciona Refactor -> Extract -> Interface.

  1. Selecciona Extraer para separar el archivo.

  1. En la ventana Extract Interface, cambia el nombre de la interfaz a TasksRepository.
  2. En la sección Miembros para formar una interfaz, verifique todos los miembros, excepto los dos miembros complementarios y los métodos privados.


  1. Haz clic en Refactor. La nueva interfaz de TasksRepository debería aparecer en el paquete de datos/fuentes.

Y DefaultTasksRepository ahora implementa TasksRepository.

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

  1. 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 interfaz TasksRepository.

FakeTestRepository.kt

class FakeTestRepository : TasksRepository  {
}

Se le indicará que debe implementar los métodos de la interfaz.

  1. Coloque el cursor sobre el error hasta que aparezca el menú de sugerencias y, luego, haga clic y seleccione Implement members.
  1. 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.

  1. En FakeTestRepository, agrega una variable LinkedHashMap que represente la lista actual de tareas y un MutableLiveData 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:

  1. getTasks: Este método debe tomar el tasksServiceData y convertirlo en una lista mediante tasksServiceData.values.toList() y, luego, mostrarlo como un resultado de Success.
  2. refreshTasks: Actualiza el valor de observableTasks para que sea el que muestra getTasks().
  3. observeTasks: Crea una corrutina mediante runBlocking y ejecuta refreshTasks y, luego, muestra observableTasks.

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.

  1. Agrega el método addTasks, que toma un vararg de tareas, agrega cada una a la HashMap 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.

  1. Abre TasksViewModel.
  2. Cambia el constructor de TasksViewModel de modo que tome TasksRepository 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.

  1. En la parte inferior del archivo TasksViewModel, fuera de la clase, agrega un TasksViewModelFactory que reciba un TasksRepository 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.

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

  1. Abre TasksViewModelTest.
  2. Agrega una propiedad FakeTestRepository a TasksViewModelTest.

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
}
  1. Actualiza el método setupViewModel para crear un FakeTestRepository con tres tareas y, luego, crea el tasksViewModel 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)
        
    }
  1. Como ya no usas el código de AndroidX Test ApplicationProvider.getApplicationContext, también puedes quitar la anotación @RunWith(AndroidJUnit4::class).
  2. 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.

  1. Abre TaskDetailViewModel.
  2. 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 }
  1. En la parte inferior del archivo TaskDetailViewModel, fuera de la clase, agrega un TaskDetailViewModelFactory.

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

  1. 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 AndroidX
  • kotlinx-coroutines-test: La biblioteca de pruebas de corrutinas
  • androidx.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.

  1. Abre taskdetail.TaskDetailFragment.
  2. 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ígenes test).

  1. 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).

  1. 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:

Esta aún no es una prueba terminada porque no se declara nada. Por ahora, ejecuta la prueba y observa lo que sucede.

  1. Esta es una prueba de instrumentación, así que asegúrate de que el emulador o tu dispositivo estén visibles.
  2. 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:

  1. Crea una clase de localizador de servicios que pueda construir y almacenar un repositorio. De forma predeterminada, construye un repositorio "normal".
  2. Refactoriza tu código para que, cuando necesites un repositorio, use el localizador de servicios.
  3. 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.

  1. Crea el archivo ServiceLocator.kt en el nivel superior del conjunto principal de orígenes.
  2. Define un object llamado ServiceLocator.
  3. Crea variables de instancia database y repository y establece ambas en null.
  4. 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:

  1. provideTasksRepository: Proporciona un repositorio existente o crea uno nuevo. Este método debe ser synchronized en this para evitar que se produzcan dos instancias de repositorio en situaciones con varios subprocesos en ejecución.
  2. createTasksRepository: Código para crear un repositorio nuevo. Llamará a createTaskLocalDataSource y creará un nuevo TasksRemoteDataSource.
  3. createTaskLocalDataSource: Código para crear una nueva fuente de datos local. Se llamará a createDataBase.
  4. 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.

  1. En el nivel superior de la jerarquía de tu paquete, abre TodoApplication y crea un val para tu repositorio y asígnale un repositorio que se obtenga con ServiceLocator.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.

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

  1. Abre TaskDetailFragement y busca la llamada a getRepository en la parte superior de la clase.
  2. 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)
}
  1. 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)
    }
  1. En StatisticsViewModel y AddEditTaskViewModel, actualiza el código que adquiere el repositorio para usar el repositorio de TodoApplication.

TasksFragment.kt

// REPLACE this code
    private val tasksRepository = DefaultTasksRepository.getRepository(application)



// WITH this code

    private val tasksRepository = (application as TodoApplication).taskRepository

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

  1. 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.
  2. Crea una clase nueva en este paquete de origen, llamada FakeAndroidTestRepository.kt.
  3. 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.

  1. Abre ServiceLocator.kt.
  2. 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.

  1. Agrega una variable de instancia llamada lock con el valor Any.

ServiceLocator.kt

private val lock = Any()
  1. 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.

  1. Abre TaskDetailFragmentTest.
  2. Declara una variable lateinit TasksRepository.
  3. 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()
    }
  1. Une el cuerpo de la función activeTaskDetails_DisplayedInUi() en runBlockingTest.
  2. 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)

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

    }

}
  1. 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):

  1. En el dispositivo de prueba, ve a Configuración > Opciones para desarrolladores.
  2. 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.

4. ViewAssertion

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.

  1. Abre TaskDetailFragmentTest.kt.
  2. 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
  1. Todo lo que esté después del comentario // THEN usará Espresso. Examina la estructura de prueba y el uso de withId, y realiza comprobaciones para ver cómo debería verse la página de detalles.
  2. Ejecuta la prueba y confirma que se apruebe.

Paso 4: Opcional: escribe tu propia prueba Espresso

Ahora, escribe una prueba tú mismo.

  1. 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
}
  1. Si observa la prueba anterior, complete esta.
  2. 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

  1. 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, como DatePicker y RecyclerView. También contiene las verificaciones de accesibilidad y la clase CountingIdlingResource, que se explicará más adelante.

Paso 2: Crea TasksFragmentTest

  1. Abre TasksFragment.
  2. 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.
  3. 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.

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

  1. Haz que tu nuevo modelo sea el NavController del fragmento.
scenario.onFragment {
    Navigation.setViewNavController(it.view!!, navController)
}
  1. 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.

  1. 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&quot).

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")
    )
}
  1. Ejecuta la prueba.

En resumen, para probar la navegación, puedes hacer lo siguiente:

  1. Usa Mockito para crear un modelo de NavController.
  2. Conecta ese NavController ficticio al fragmento.
  3. 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.

  1. Escribe el clickAddTaskButton_navigateToAddEditFragment de prueba que verifica que, si haces clic en el BAF + +, navegues a AddEditTaskFragment.

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.

Download Zip

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:

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.