(Parte III) Una breve historia con RxSwift, programando una aplicación iOS desde 0.

En esta tercera entrega, entramos de lleno en los tests unitarios en RxSwift. Si acabas de aterrizar, tienes aquí el enlace al capítulo anterior.

Antes de empezar a testear, tenemos que tener un código testeable. Y me da a mi que el código anterior no es testeable. Mientras lo preparamos, haremos lo siguiente.

Si el valor de álbumes tiene alguna información la tabla pintará la información tal y como la tenemos. En cambio, si el valor de álbumes devuelve un error, debido por ejemplo, un 500 típico de un error de API, entonces que la tabla se pinte con una celda que diga que ha habido algún error.

En esta primera parte, realizaremos esto y preparemos el código para que sea testeable. En la segunda parte, crearemos unos tests sencillos para entender la filosofía de RxSwift. Vamos para allá.

Hasta ahora, con RxDataSources, el bind empleado, permite solamente usar un tipo de celda. Lo que vamos ha hacer es que se permitan usar más de un tipo de celda y, en función, del objeto recibido, la tabla se pintará con unas celdas o con otras.

Lo primero será crear una celda nueva. La llamaremos MessageTableViewCell y tendrá este aspecto.

Esta celda tiene un mensaje “Unknown error” que se pintará de color rojo. También, hemos actualizado el componente Label.

RxDataSources permite crear tu propio datasource. Para ello, necesitamos crear un nuevo modelo de datos, de manera, que lo entienda. Es muy sencillo hacerlo, si tenéis algún problema, revisen los ejemplos que tienen en su github.

Tenemos que crear dos ficheros nuevos, los llamaremos ListSectionModel y ListSectionItem.

El primero es un enum que representa los diferentes tipos de secciones que tendrá la tabla.

El segundo es un enum con los diferentes tipos de celda que puede tener una o más secciones.

Existen dos tipos de datasources. Nosotros usaremos el más sencillo. Ante cualquier emisión nueva, la tabla hará un reloadData.

Y, por último, actualizaremos el bind a la tabla.

Pero ocurre algo. El modelo de datos ha cambiado. No acepta un array de álbumes, ahora tiene que ser un array de secciones, de ListSectionModels.

En la carpeta Domain, tenemos la entidad Album. Crea una carpeta llamada Entities y pon dentro el fichero Album. Ahora crea otra carpeta llamada UseCases y crea dentro un fichero llamado SearchUseCase. Mueve el contenido de álbumes ahí. En breve hablaremos de Dominio, Entidades y UseCases.

Ahora actualizemos el viewModel de la siguiente forma. Revisa el modelo del datasource creado. Necesitamos una sección de tipo .section que contenga elementos de tipo .search en el que su viewModel sea de tipo Album. Algo así se nos quedaría.

Un poco raro se queda esto. Algo podríamos hacer para que fuera un poco más legible.

Aquí estamos añadiendo un constructor a la sección, de manera que nos construya una, pero, en realidad, lo que queremos no es construir un elemento, sino un array. ¿Entonces?

Extendamos Array para que tengamos un constructor propio, de manera que nos construya el objeto que necesitamos en el ViewModel.

Ya lo tendríamos.

Habrás visto que la forma de instanciar el caso de uso Search, no está del todo bien. Pero antes una cosa. Acostúmbrate siempre a separar por capas cualquier aplicación, por muy grande o pequeña que sea.

En la carpeta Application, estoy poniendo ahí dentro todo lo que tenga que ver con la capa de aplicación, esto es, pantallas únicamente. Un ViewController es una pantalla. Si uso MVVM, tendré además un ViewModel. Si quiero usar VIPER, por ejemplo, tendré un Router y un Interactor, sí, en esa capa. VIPER es un patrón en la capa de aplicación y un VIPER representa una sola pantalla.

Lo que comúnmente se denomina Dominio son las funcionalidades de la aplicación. En nuestro caso, de momento, tenemos una entidad Albumes y estamos haciendo una funcionalidad. De momento, está hardcoded. ¿Por qué usar una capa intermedia para separar lo que bien sería una llamada API? Por lo de siempre, porque, no sabemos si el día de mañana cambiará. De esta forma, nos aseguramos que la capa de Aplicación está totalmente aislada de los requisitos de negocio.

Volvamos a la instancia y entramos de lleno en testear. Si queremos testear, tenemos que usar interfaces para poder crear objetos de test. En este caso, esos objetos se llamarán Mocks. Si nunca has testeado antes, te recomiendo que busques algún tutorial.

Yo aprendí con este libro.

https://www.packtpub.com/product/test-driven-ios-development-with-swift-4-third-edition/9781788475709

Aquí me voy a centrar en cómo testear con RxSwift solamente.

Crearemos el entorno de test. Si no lo creaste al crear el proyecto. Lo puedes crear, añadiendo un target nuevo y seleccionando el Unit Test Bundle.

Se crea una carpeta nueva con un fichero de test, bórralo. Crea ahí dentro una carpeta llamada Application, dentro una llamada List y dentro, un fichero de tipo Unit Test llamado ListViewModelTests.

Tenemos que actualizar el fichero pods y añadir dos pods nuevos.

Hacemos pod install. Abrimos el fichero nuevo de tests y borramos el contenido de la clase.

Lo primero, ¿qué queremos testear? El ListViewModel.

El ListViewModel tiene dos getters, por lo tanto, con RxTest, necesitaremos dos tests. Pero, ahora mismo, tenemos el UseCase hardcodeado. Crearemos un protocol.

Actualizaremos el viewModel, de esta forma.

Y, actualizaremos, el SceneDelegate.

Nota: conoceréis la Dependency Injection, así que no me entretengo. Imagínense una aplicación con muchísimos UseCases y muchas dependencias por instanciar. Así que cuando la aplicación crece mucho, necesitamos una estrategia para evitar que esto se convierta en un problema.

Os dejo un artículo muy interesante sobre la DI en Swift.

Sigamos, una vez con esto. Ya podríamos crear un mock del caso de uso.

En RxTest, necesitamos sincronizar todos los elementos bajo la clase TestScheduler, así que pasaremos dicha referencia por el constructor.

Para mockear la clase, crearemos un mock de observable, en este caso un coldObservable y le diremos que, en un tiempo 10, este objeto devolverá un array vacío y en un tiempo 20, que devuelva un array con un solo valor de tipo álbum. Un fake en este caso.

Si volvemos al inicio de la clase, se nos quedaría así.

Al objeto SUT, le pasamos el objeto mock y instanciamos el scheduler en 0.

El primer test, lo tendríamos así.

¿Pero qué ocurre? Cuando no tenemos nada, el viewModel está devolviendo una sección vacía, ¿es eso lo que queremos? Puede que si, puede que no.

Actualizaremos el viewModel así.

El test ya pasaría. Pero, …, ¿la responsabilidad del viewModel es comprobar si albums es o no vacío? Pues no, es del constructor de secciones.

Ahora mucho mejor. Recuerda dejar el viewModel como estaba antes y comprueba que el test sigue funcionando.

Nos queda un test más. Éste es sencillo.

Una vez entendido cómo se testea y qué cosas podrían ocurrir si no testeamos. Vamos a ver qué le puede pasar a la aplicación si aparecen errores.

Un Observable es un canal que está observando algún tipo y que por ese canal pueden pasar una o más emisiones. Nos interesan tres tipos, básicamente, el evento Next significa que se emite un valor. El evento Error significa que se emite algún tipo de error. Por último, si completa, se envía un evento completado y ese canal finalizará su ejecución para siempre.

En el primer artículo hablamos de just. Pues just es un evento que lanza un next con el valor definido y, acto seguido, completa.

En el mock que hemos creado antes, lo hemos definido como un observable que emite en un tiempo 10 algo y luego en un tiempo 20 algo más. Podríamos añadir .error o .completed y bien compilaría.

Crearemos en la carpeta de Entities de Dominio, el archivo ApplicationError.

Ahora en el SearchUseCase, cambiaremos los álbumes.

Ejecutamos y ¿qué pasa?

Falla, porque el bind no acepta errores. ¿Qué podríamos hacer? Proteger el bind.

Y, tendríamos, el siguiente resultado. Recuerda que catchAndReturn permite transformar ese error al valor que tu decidas y acto seguido el stream morirá.

Podríamos mejorar un poco el código. Añade esto en ListSectionModel

Y actualizamos el catchAndReturn

Es importante conocer bind, más adelante presentaremos los drive, que son más seguros.

Pues aquí tenemos ya todo. ¿Qué hemos aprendido en esta tercera parte?

  • Podemos crear tablas con diferentes tipos de tabla.
  • Testear es más importante de lo que parece. Incluso nos ayudará a tener diferentes escenarios en un mismo test.
  • Tener una capa de dominio con las entidades necesarias para trabajar y una capa de aplicación con sus modelos diferenciados en un lado y en otro, te ayudará a tener una aplicación más ordenada.

Por último, vamos a dejar el UseCase un poco más divertido.

El operador flatMap nos ayudará a transformar un Observable. En este caso, hemos creado un contador que hará una emisión del número de contador. Con el flatMap conseguimos transformarlo. Empezará emitiendo álbumes cada segundo en un órden aleatorio. Cuando llegue a 10, se emitirá un error. Y el observable morirá.

Ya tenemos todo, el código fuente de este tercer artículo, está aquí.

Hasta la próxima.

--

--

mobile developer

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store