Rapidez y confiabilidad usando Consumer Driven Contract Testing

Este artículo sale de desgranar el video https://www.youtube.com/watch?v=nQ0UGY2-YYI donde Alon Pe’er describe cuál ha sido su experiencia trabajando en SoundCloud, en los comienzos de la integración de la metodología de Contract Testing.

Si bien es un vídeo con información y conceptos valiosos, hay ciertas cosas que en 2017 aun estaban por definir, y a día de hoy son problemas resueltos.

Testing con monolitos

Back end y front end en el mismo repositorio. Back end contenía todo lo necesario para el funcionamiento de la aplicación al completo.

Un único servicio, un único cliente… los cambios en el API eran sencillos. Migraciones o versiones de API deprecadas eran gestiones triviales porque no había complejidad en el sistema.

Las cosas empiezan a complicarse cuando se empiezan a incorporar nuevos clientes. Además de la web, aparecen dispositivos móviles iOS y Android y cada uno de estos clientes tienen nuevos requisitos.

El código en el backend empieza a complicarse generando código espagueti, y se hace difícil desplegar sin romper al menos uno de los clientes.

Estos problemas se intentan paliar añadiendo más testing manual, el cual, además de ser caro, lento y expuesto a errores humanos, no escala.

Y también se tendía a añadir más test end-to-end, los cuales son extremadamente complicados de mantener, lentos, no muy confiables y generadores de cuellos de botella.

Testing con microservicios

Estrategia de testing habitual.

Donde generalmente tenemos una amplia suite de test unitarios, algo menos de test de integración y menos end-to-end.

Razones habituales de las pérdidas de servicio después de analizar las incidencias.

20% era porque alguien desplegaba sus cambios en su API, y estos cambios rompían en alguno de sus consumidores.

Normalmente las explicaciones que se obtienen explicando la razón de estos fallos son que no se tiene la suficiente confianza en los test de aceptación y que esto genera que se ignoren los warning o incluso los errores que estos test puedan indicar.

Parece por tanto que esto es algo a lo que se le debería prestar atención para ponerle solución.

¿Por qué los test unitarios no son suficiente?

Básicamente por que hay ciertos escenarios que los test unitarios no son capaces de cubrir, y que generarían problemas reales en producción.

Todo se ve siempre mejor con un ejemplo, ¿no? Vamos a ello!

En nuestro código tenemos un endpoint GET /cake y vamos a usar la librería JsonLibFoo para devolver el JSON del objeto current_state.

Así mismo, en nuestros test, usamos la misma librería para evaluar que cuando el json que devuelve la llamada al servicio /cake se convierte a objeto, éste contiene el valor lie.

Ahora imaginemos que se cambia la librería JSON utilizada y se pasa a usar JsonLibBar, la cual en vez de de-serializar usando el carácter guión bajo como separador, utiliza camel case.

En este caso los test unitarios no son útiles, porque seguirán pasando, pero lo que querríamos es que fallaran.

¿Solución? Contract Testing

Esto puede ser resuelto separando consumidor y proveedor y creando test unitarios de forma independiente.

Proceso de definición de contrato en el consumidor

El consumidor definirá unos test definiendo lo que espera del proveedor, y esto es lo que generará el contrato, que posteriormente deberá ser validado en el proveedor.

El funcionamiento de Contract Testing tiene siempre la misma base teórica, y puede ser implementado como se considere. Pact es un framework que facilita una serie de herramientas para permitir la implementación de Contract Testing, y es lo que se suele utilizar en el mercado para llevar a cabo esta metodología de testing.

Proceso de verificación en el proveedor

El proveedor debe acceder a todos los contratos que se hayan generado en los cuales él esté involucrado, y deberá validarlos.

Es probable que el proveedor haga uso de un sistema de almacenamiento, o se apoye en otros servicios.

Debemos hacer que el proveedor esté aislado al 100%.

Lidiar con estados en base de datos

En muchas ocasiones los test pueden necesitar de cierta información en la base de datos para poder funcionar.

Pact proporciona lo que llama states, y que será ejecutado siempre antes de cualquier test, para situar al proveedor en el estado que se necesite para que los test puedan ser ejecutados de forma satisfactoria.

Imaginemos que queremos probar el endpoint GET /likes/{user-id}, y definimos un test en el cual decimos que cuando se llame a GET /likes/1000, queremos que la respuesta sea un 200, y que el usuario tenga 2 likes.

Para que esto pueda realizarse de forma satisfactoria previamente deberíamos configurar el sistema para que el usuario 1000 realmente tenga dichos likes, así que previamente a ejecutar nuestro test, Pact insertará los likes para dicho usuario.

Dependencias entre microservicios

Untitled

Continuous Integration pipelines

Alon menciona que el siguiente pipeline es el que usaban para la automatización en la parte del consumidor. Este es el punto que más me llamó la atención cuando vi el vídeo, porque comenta un déficit que para mi es esencial poder cubrir con la implantación de Contract Testing.

Untitled

Comenta Alon que en esas fases faltaría ser capaz de verificar que el proveedor realmente va a ser compatible con el nuevo contrato que hemos definido.

¡Imagina que estamos definiendo el uso de un nuevo endpoint del proveedor, pero que éste aun no ha sido implementado!

Deberíamos ser capaces de verificar que esto es así, para no poder desplegar el consumidor si el proveedor no nos ha validado.

A día de hoy podríamos solucionar esto de la siguiente manera.

Untitled
  • Un trigger que se ejecutará cada vez que un nuevo contrato del consumidor sea publicado, empezando un pipeline en el proveedor.
  • Se ejecutarán los test del proveedor
  • Se llamará a la herramienta can-i-deploy en el proveedor, verificando si todos sus consumidores son compatibles con él.
  • Se ejecutará la misma herramienta can-i-deploy, pero esta vez en el consumidor, y, en el caso expuesto anteriormente, dicha herramienta frenaría el pipeline, ya que no se encontraría compatibilidad con el contrato que se generó.
Untitled

Advertencias a tener en cuenta

  • No excluye la comunicación y la gestión entre distintos equipos.
  • El Framework es relativamente nuevo, y aunque cada vez es más estable, y está más probado en el sector, hay que tener este punto en cuenta.
  • Se necesita de una pequeña curva de aprendizaje para conocer su uso y realizar su implantación.
  • Hay que estar convencido de sus ventajas como equipo y en general. No es algo que se puede implantar en un equipo de forma independiente. Necesita la aprobación y aceptación de todos los equipos y servicios que conforman el sistema para ser eficiente.
  • La configuración para aislar el proveedor de otros sistemas puede ser en ciertos casos algo tediosa. Aunque bien es cierto que es un trabajo de una única vez.
  • Automatizar la verificación del proveedor cuando hay cambios en el consumidor es algo que se necesita para garantizar el correcto funcionamiento. Uso del can-i-deploy.