Programación Reactiva y Testable con Retrofit2

Cómo ya sabéis, me encanta trabajar con APIs JSON, así que como ya viene siendo habitual en mis artículos, me voy a basar en el consumo de una API que he definido a modo de prueba en la aplicación que ofrece mockable.io.

Esta tiene una simple y única llamada que nos proporciona algunos datos sobre varias ciudades. Veamos un ejemplo:

Lo primero que vamos a hacer es definir un objeto que nos permita representar las estructuras de datos devueltas por la API:

Una vez que ya tenemos definida las estructuras de datos devuelta por la API, vamos a implementar el servicio que va a consumirla. Por el momento solo implementaremos la única llamada habilitada, no obstante, podríamos extender el mismo desarrollo para el resto de llamadas.

En esta ocasión vamos a trabajar con la librería Retrofit2, la cuál nos proporcionará una interfaz de alto nivel para la realización de las peticiones web necesarias. Además también vamos a hacer uso del objeto Observable de las Reactive eXtensions con tal de orientar nuestro desarrollo a un aproximación reactiva.

Finalmente vamos a crear una clase Application que será la encargada consumir el servicio ApiService y por lo tanto la que obtendrá el resultado interpretado de la llamada a la API.

Y hasta aquí podría llegar nuestro artículo, puesto que con estos tres pedazos de código ya disponemos de un ejemplo de programación reactiva con Retrofit2. Simple e intuitivo. Sin embargo, tal y como lo hemos definido al principio, nuestro objetivo no era solo obtener un código reactivo sino que también fuera testeable. Para eso resulta imprescindible escribir, al menos, un par de tests que nos permitan comprobar que nuestro código funciona correctamente.

El resultado

Antes de escribir un test (incluso antes de escribir el código principal… TDD, ejem…) deberíamos tener clara cuál es la intención de nuestro código, qué problema quiere resolver, qué tarea quiere realizar, etc.

En este caso, nuestra intención era centrarnos en el enfoque reactivo y testeable de nuestro código, por eso, la business logic es prácticamente inexistente:

  • Hacer una petición a la API anteriormente definida
  • Escribir por salida estándar (consola) el nombre de las ciudades devueltas.
LAS PRUEBAS

En esta ocasión vamos a escribir solo dos tests, a modo de ejemplo, pero desde dos perspectivas diferentes, de esta manera podremos apreciar las diferencias que existen en casa caso. Posteriormente podríamos extender nuestra batería de tests a más casuísticas, por ejemplo, cuándo la respuesta del servidor corresponde a un error o cuándo la lista de ciudades devuelta por el mismo está vacía. Sin embargo, esto quedará fuera del alcance de este artículo y como ejercicio para el lector.

La primera perspectiva nos permitirá probar el código responsable de convertir la respuesta HTTP devuelta por el servidor a estructuras de datos interpretadas por la JVM. Para ello, vamos a interceptar la llamada HTTP y vamos a asumir que el contenido JSON devuelto es el que nosotros queramos proporcionar.

La segunda perspectiva nos permitirá probar el código responsable ejecutar la lógica de negocio, en este caso prácticamente inexistente, pero no menos importante. De hecho, lo habitual será que la lógica de negocio suela ser lo que aporte valor a nuestra aplicación y no las llamadas HTTP y demás operaciones que en la mayoría de casos resultarán ser simples herramientas de consumo. Para dar este enfoque a nuestras pruebas, lo que vamos a hacer será asumir que nuestro código ya tiene un objeto correctamente construido, es decir, que la prueba anterior ha sido superada con éxito.

PROBANDO LA LÓGICA DE NEGOCIO

Es decir, que los nombres de las ciudades recogidas de la API sean escritas por la salida estándar (consola). Para ello vamos mockear nuestro ApiService para qué devuelva el objeto que entendemos que debería devolver en caso de que la respuesta del servidor haya sido la esperada.

*Es importante recordar que resulta fundamental nombrar correctamente a nuestras pruebas, ya que en caso contrario éstas perderán efectividad, puesto que nos seguirá siendo difícil encontrar que parte de nuestra aplicación ha sido rota.

Como podemos ver en el anterior ejemplo, lo que vamos a hacer para realizar este tipo de prueba es inyectar un mock en lugar de la dependencia original para simular que el resultado del mismo es el esperado cuándo el mismo ha funcionado correctamente. De otra forma, al ejecutar estaremos verificando qué “si la llamada funciona correctamente, nuestro código hace lo que toca”. En este caso, escribir por pantalla el nombre de las ciudades.

Interceptando la petición HTTP

Aquí viene la parte fundamental de lo que podríamos considerar “código testable”. Puesto que, hasta ahora, hemos tenido que recurrir a pocos cambios de nuestro código para poder escribir las pruebas oportunas. Sin embargo, la cosa se complica cuándo queremos “meter mano” a las herramientas externas, como es el caso de Retrofit2.

Lo primero que vamos a hacer es definir una variable en nuestra clase Application que nos permita definir si queremos trabajar con conexión o no. Entenderemos como trabajar sin conexión cuándo las peticiones sean interceptadas por nuestro código y, por lo tanto, que no haya interacción real con la red.

Por el momento la definimos como true, puesto que ahora no vamos a querer trabajar con el servidor de “producción”.

Lo siguiente será implementar la interfaz de Interceptor que nos proporciona la librería OkHttp3 (dependencia de Retrofit2):

De nuevo estamos ante un ejemplo simple. Pero, como siempre, podríamos extender este mismo ejemplo para un caso más complejo. Por ejemplo, podríamos tener varias FAKE_RESPONSE preparadas y responder una u otra en función del path de la petición. Es decir, que si, por ejemplo, la petición se realiza a “v1/countries”, que la respuesta sea una FAKE_RESPONSE_2.

Como podemos observar en el código de nuestro FakeInterceptor (que quizás no es el mejor nombre jejeje), estamos haciendo que la respuesta “fake” solo sea devuelta cuándo la aplicación esté trabajando en modo offline. En caso contrario, la aplicación seguirá trabajando con el resultado obtenido de la petición al servidor.

Finalmente solo nos falta sustituir el anterior companion object definido en nuestro ApiService, por el siguiente:

Dónde únicamente cambian un par de líneas dónde se define el cliente que será utilizado para la petición HTTP, la cuál será interceptada y atendida manualmente.

Ahora ya podemos añadir nuestro nueva prueba a nuestra suite y comprobar que la misma finaliza con éxito.

Para acabar, solo nos faltaría implementar algún mecanismo de gestión de variables para el entorno de pruebas, por ejemplo, separando las pruebas offline de las pruebas online. Sin embargo, en este sentido dedicaré un artículo completo acerca de las suites de pruebas más adelante.

Gracias!