Cómo hacer tests de Corrutinas y Flows – Paso a Paso
Antonio Leiva

¡Vaya viaje por el que hemos pasado en estos artículos! Hace ya varios de ellos empezamos hablando sobre la programación reactiva con Flow, y hemos aprendido un montón de conceptos e ideas sobre cómo aplicarlos en el día a día.

Pero nada de esto está completo si no explicamos cómo hacer tests. Así que el artículo de hoy irá sobre esto.

Cómo hacer tests de corrutinas

Aunque las corrutinas ya llevan un tiempo entre nosotros, sí que es verdad que hacer tests sobre ellas es algo sobre lo que no se ha trabajado a fondo hasta muy recientemente.

De hecho, varias de las cosas que vamos a ver hoy aquí son aún experimentales, y aunque tengo el presentimiento de que pronto dejarán de serlo, todavía hay algunas cosas que pueden cambiar.

El primer problema con el que nos vamos a encontrar cuando intentamos hacer tests de corrutinas es el del Main Dispatcher. En los tests no existe el hilo principal de Android, y si lo usas, tendrás una excepción.

Si estás usando tu propio CoroutineScope, esto es algo que puedes cambiar simplemente pasando otra dependencia. Pero si usas viewModelScope como es nuestro caso, la cosa no es tan fácil.

La librería de testing de corrutinas nos permite cambiar el Main Dispatcher duante los tests. Para usar esta librería, importa la siguiente dependencia:

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9'

Creando una Rule para los tests de corrutinas

Para poder hacer lo que hablábamos, vamos a necesitar realizar unos pasos justo antes y después de cada test. Esto lo podríamos hacer en la clase de test con las anotaciones @Before y @After, pero luego nos tocaría copiar y pegar ese código en todas partes, o usar herencia…

Hay una forma más sencilla usando composición, que es el uso de las Rules de Testing. Con las Rules, lo que conseguimos es ejecutar un código antes y después de cada test, y para incluirlo en la clase solo hay que usar la anotación @Rule.

Para ello, nos creamos una clase que here de TestWatcher:

class CoroutinesTestRule: TestWatcher() {
   ...
}

Vamos a necesitar sobrescribir dos funciones: una que se ejecuta antes, y otra después de cada test:

class CoroutinesTestRule: TestWatcher() {

    override fun starting(description: Description?) {  }

    override fun finished(description: Description?) { }
}

Además, necesitas un dispatcher que sustituya al Main. Para ello puedes usar la clase TestCoroutineDispatcher:

private val testDispatcher = TestCoroutineDispatcher()

Ahora lo que haremos será asignarle este dispatcher al Main en la primera función, y resetearlo en la segunda. Además, en la segunda reinicializaremos el dispatcher para eliminar cualquier corrutina que se pueda estar ejecutando:

class CoroutinesTestRule : TestWatcher() {

    private val testDispatcher = TestCoroutineDispatcher()

    override fun starting(description: Description?) {
        super.starting(description)
        Dispatchers.setMain(testDispatcher)
    }

    override fun finished(description: Description?) {
        super.finished(description)
        Dispatchers.resetMain()
        testDispatcher.cleanupTestCoroutines()
    }
}

Hacer un test de un Flow

En realidad testear un Flow es tan sencillo como testear cualquier otra función suspendida, así que lo que te voy a contar aquí es general para cualquier función suspend.

Carga la Rule que creamos antes en la clase de test:

class MainViewModelTest {

    @get:Rule
    val coroutinesTestRule = CoroutinesTestRule()

}

Lo primero que necesitas es rodear tu test con el builder runBlockingTest. En algunos casos podría valer con un runBlocking normal, pero usar el de tests no te hace ningún mal y te puede dar ventajas en el futuro.

@Test
fun `Listening to movies Flow emits the list of movies from the server`() = runBlockingTest {
    ...
}

Esto conseguirá que, por mucho que las corrutinas hagan cosas asíncronas, el test no acabe hasta que no hayan finalizado esas corrutinas.

El resto que hace este test es inicializar el ViewModel con un repositorio que tiene dos fuentes de datos fake, con el objetivo de controlar los posibles resultados.

Finalmente, se hace un collect sobre el Flow y se esperan los resultados, donde se comprobará que llegan los que se esperan.

@Test
fun `Listening to movies Flow emits the list of movies from the server`() = runBlockingTest {
    val repository = MoviesRepository(FakeLocalDataSource(), FakeRemoteDataSource(fakeMovies))
    val vm = MainViewModel(repository)

    vm.movies.collect {
        Assert.assertEquals(fakeMovies, it)
    }
}

Dando saltos en el tiempo con los tests de corrutinas

¿Sabías que con las corrutinas, puedes hacer que los tests viajen en el tiempo? Parece ciencia ficción, pero es real y algo muy útil.

Imagina que necesitas probar que una petición lanza una excepción cuando se produce un timeout.

Lo primero es que, con corrutinas, introducir un timeout en una petición es tan sencillo como usar la función timeout(). Por ejemplo, en checkRequireNewPage()del repositorio, podríamos hacer que la petición al servidor tuviera un timeout:

val newMovies = withTimeout(5_000) { remoteDataSource.getMovies(page) }

¿Cómo podríamos probar que este timeout funciona?

Durante los tests, nos podemos crear un FakeRemoteDataSource que permita simular peticiones que tardan mucho tiempo:

class FakeRemoteDataSource(
    private val movies: List<Movie> = emptyList(),
    private val delay: Long = 0
) : RemoteDataSource {

    override suspend fun getMovies(page: Int): List<Movie> {
        delay(delay)
        return movies
    }
}

La función delay() es bastante especial, porque es un como Thread.sleep() en el mundo de las corrutinas, pero nos permite avanzar y rebobinar durante los tests. Así que ahora podríamos escribir un test que espera una excepción:

@Test(expected = TimeoutCancellationException::class)
fun `After timeout, an exception is thrown`() = runBlockingTest {
    ...
}

Luego configurar el repositorio con un tiempo de espera de 6 segundos, y llamar al repositorio

val repository = MoviesRepository(
    FakeLocalDataSource(),
    FakeRemoteDataSource(delay = 6_000)
)

repository.checkRequireNewPage(0)

Y aquí viene la magia. Ahora podemos decirle “avanza el tiempo de la corrutina x milisegundos”. En nuestro caso van a ser 5 segundos para que salte el timeout:

advanceTimeBy(5_000)

Esto es buenísimo, porque en vez de tener que esperar 5 segundos a para que salte la excepción, con lo que ello conlleva (perdemos 5 segundos de ejecución de tests sin hacer nada), avanzamos el tiempo, y el test se ejecuta de forma instantánea, pero la prueba es igual de válida.

Aquí puedes ver que el test ha tardado 189ms:

Te dejo aquí el código completo del test:

@Test(expected = TimeoutCancellationException::class)
fun `After timeout, an exception is thrown`() = runBlockingTest {
val repository = MoviesRepository(
    FakeLocalDataSource(),
    FakeRemoteDataSource(delay = 6_000)
)

    repository.checkRequireNewPage(0)

    advanceTimeBy(5_000)
}

Conclusión

Como ves, con las nuevas herramientas de testing de corrutinas, hacer tests se vuelve mucho más sencillo, y además tenemos una potencia que pocas herramientas pueden proporcionar.

Si quieres ver el código completo, puedes ir al repositorio de Flow.

Por el momento, terminamos con esta serie de corrutinas con Flow. Si tienes cualquier sugerencia o algo en lo que te gustaría que ahondase, te leo en los comentarios.

¡Un abrazo!

Quizá también te interese…

Kotlin 1.5.0 : Las 5 novedades que puedes empezar a usar hoy

Kotlin 1.5.0 : Las 5 novedades que puedes empezar a usar hoy

Kotlin 1.5.0 ya está aquí, y como siempre trae una serie de novedades que te van a interesar muchísimo. Cabe destacar que a partir de ahora, de acuerdo las nuevas versiones de Kotlin se lanzarán cada 6 meses, independientemente de las nuevas funcionalidades que...

¿Qué es Kotlin y para qué sirve?

¿Qué es Kotlin y para qué sirve?

Kotlin es un lenguaje de programación de código abierto creado por JetBrains que se ha popularizado gracias a que se puede utilizar para programar aplicaciones Android. Pero si has llegado hasta aquí pensando que Kotlin solo se puede usar en Android, lo que te voy a...

2 formas de recolectar Flows en la UI que SÍ funcionan

2 formas de recolectar Flows en la UI que SÍ funcionan

En la serie de artículos sobre Programación Reactiva con Flow hemos visto muchos conceptos, y hemos aprendido cómo aplicarlos al desarrollo Android. Pero hay algo que no hemos hecho del todo bien. Esto es la recolección de Flows desde la Activity (o el Fragment, en...

0 comentarios

Enviar un comentario

Los datos personales que proporciones a través de este formulario quedarán registrados en un fichero de Antonio Leiva Gordillo, con el fin de gestionar los comentarios que realizas en este blog. La legitimación se realiza a través del consentimiento de la parte interesada. Si no se acepta, no podrás comentar en este blog. Los datos que proporciona solo se utilizan para evitar el correo no deseado y no se usarán para nada más. Puede ejercer los derechos de acceso, rectificación, cancelación y oposición en contacto@devexperto.com.

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Acepto la política de privacidad *