Contract Testing está pensado para ayudar en la verificación y validación de la integración de componentes independientes que se comunican entre sí. Estos componentes pueden ser microservicios.
Esta vez en lugar de soltarte un rollo teórico sin más, te quiero contar una historia, y tú vas a formar parte de ella. ¡Presta atención!
Problemas en la integración de microservicios
Trabajas para una gran empresa que mueve cantidades abrumadoras de dinero cada minuto. Lo hace gracias a una serie de microservicios, gestionados por distintos equipos al 100%, desde las fases iniciales, hasta su mantenimiento en producción.
En esa cantidad ingente de servicios que conforman el sistema completo, tú eres el responsable del servicio de facturación, que se encuentra entre los más importantes de la compañía, porque es donde se mueve el money y eso a fin de cuentas, es lo que interesa.
Tu servicio de facturación utiliza, entre otros, el microservicio de usuarios, el cual gestiona otro equipo al que apenas conoces, porque no has tenido necesidad de trabajar con él desde que te incorporaste en la empresa.
El servicio de usuarios expone varios end points, pero tu servicio de facturación sólo utiliza uno de ellos, y de hecho, sólo hace uso de un campo en su respuesta. Un campo llamado “usuario”.
Todo iba como la seda, sin ninguna incidencia en producción desde hacía unos meses.
Hasta que un viernes a última hora (cuando más gusta)… te empiezan a entrar llamadas y mensajes a tu móvil de empresa con alarmas de PagerDuty indicando que tu servicio de facturación estaba fallando.
Además de entrarte los 7 males, con los nervios habituales ante este tipo de situaciones, donde la empresa está perdiendo miles y miles de euros por cada minuto que tu servicio está inestable, tratas de pararte a pensar …
“Qué raro… hace tiempo que el servicio está estable y el trabajo de mi equipo se estaba desarrollando en otros servicios… No hemos cambiado nada en el servicio de facturación! ¿qué puede estar pasando?”
Sigues centrado en tu servicio, y en qué puede haber cambiado en él, pero no eres capaz de sacar conclusiones.
Hasta que, después de indagar en logs y más logs, y de perder un tiempo muy valioso en ello, empiezas a pensar en que quizás no es un error causado propiamente por tu servicio, sino que algún cambio en un sistema externo que tu servicio utiliza pueda estar causando el problema.
Es entonces cuando llega el momento ‘ajá’, entras en el repositorio del servicio de usuarios y ves que efectivamente hubo un cambio el viernes por la tarde que fue desplegado a producción.
Se había cambiado la respuesta en uno de sus endpoints modificando uno de sus cambios de “usuario” a “usuarios”. ¡Habían cambiado una letra! ¡UNA letra!
Y ese cambio de una letra fue lo que provocó que tu servicio de facturación dejara de funcionar y te comenzaran a brear con incidencias en producción. Lo mejor de todo es que sin haber hecho ningún cambio por tu parte.
Hasta aquí lo que perfectamente podría haber sido una historia real, y que de hecho seguro que lo ha sido para más de uno.
Vamos a analizar un poquito más la historia y ver qué podemos aprender de ella.
¿Por qué los test no encontraron este problema de integración de microservicios?
Esta es la primera pregunta que, una vez resuelta la incidencia, todos los líderes se preguntaron.
Todo el mundo sabe que deberían haberlo hecho, pero no lo hicieron.
¿Por qué? Todo el mundo piensa que de entre todos los test que un servicio podría/debería tener:
- Test unitarios simulando dependencias.
- Test de integración.
- Test end to end.
¡El error debería haberse detectado al menos en los test de integración!
Y la respuesta es que sí, pero… el problema principal es que… ¡No se implementan test de integración!
Vamos a ver qué ocurre con todo ese tipo de test que todo el mundo entiende como necesarios para un microservicio.
Test unitarios simulando dependencias
El problema con esos test es que el servicio de usuarios es sólo una simulación, y somos nosotros, desde el servicio de facturación los que decidimos cuál es la respuesta que esperamos recibir, asumiendo que así será cuando vayamos a producción. Y este es precisamente el problema de este tipo de test. Las presunciones, que en muchas ocasiones pueden no ser correctas, pudiendo provocar problemas cuando la integración de microservicios ocurra en producción.
De la misma forma, los servicios que llaman al servicio de facturación, utiliza la misma filosofía para probar.
Por lo tanto, en el servicio de usuarios, cuando se realizó este cambio, este tipo de test deberían de haber saltado. Y efectivamente, así lo hicieron. Pero el desarrollador simplemente los arregló para que pasaran.
Y de nuevo volvemos al problema principal de estos tests que utilizan simulaciones de servicios externos, y es que se basan en presunciones, que muchas veces no son ciertas. Y nadie o nada valida que estas suposiciones sean ciertas.
Por lo tanto podríamos concluir los siguientes puntos:
Cuando se simulan otros sistemas, las suposiciones pueden ser falsas y no se van a validar, pudiendo provocar no detectar problemas que ocurrirán en producción.
Son baratos, ya que no tienes ninguna dependencia y se pueden empezar a implementar desde el momento cero.
Muy rápidos de ejecutar, ya que únicamente se prueba de forma unitaria lo que se necesita.
Cuando no existen dependencias externas son sólidos y muy confiables.
Siempre que fallen, el error va a ser específico y apuntando de forma clara a dónde se debe mirar.
Test de integración
Como su propio nombre indica, deberían probar la integración de microservicios.
Por cada dependencia de tu servicio de facturación, se debería levantar el servicio junto con su dependencia, y lanzar la suite de test para verificar que la integración entre ambos microservicios es correcta.
Son confiables porque se ejecutan sobre sistemas reales.
Son costosos, porque exigen una labor de alineación con distintos equipos, gastando horas y horas de reuniones.
Son lentos, precisamente porque ejecutan flujos reales.
No son confiables debido a que se necesita de toda esta infraestructura, es muy sencillo que los test den falsos positivos, por problemas que nada tienen que ver con los cambios de código.
Es muy difícil saber qué es lo que fue mal en caso de error, porque deberías investigar en toda la integración para llegar a conocer el problema, generalmente algo que lleva mucho tiempo.
Así que uniendo todos estos puntos, el equipo de desarrollo puede perder horas y horas de forma inútil, simplemente repitiendo los test hasta que se obtiene, de forma milagrosa, una ejecución satisfactoria.
Y debido a esto, en muchas ocasiones se decide simplemente no implementar este tipo de test.
Este fue el caso en nuestra historia, y de ahí que no saltaran las alarmas antes de que el servicio de usuarios estuviera sirviendo en producción.
End-to-end tests
Los test end-to-end involucran a todos los servicios del sistema al completo. Generalmente necesitan de un entorno dedicado, donde todos los servicios se puedan levantar.
Este tipo de test tiene exactamente los mismos inconvenientes que los de integración, incluso se podría considerar que a un nivel mayor.
Una mejor aproximación para probar la integración de microservicios → Contract Testing
En una mano tenemos la posibilidad de tener unos test lentos, poco confiables, difíciles de depurar y caros, pero que al menos dan algo de seguridad… cuando funcionan.
En la otra mano tenemos la opción de no pasar por ese calvario, aunque suponga perder esa seguridad, y simplemente decidir ´ir con cuidado´ con cada cambio y rezar a los dioses del código para que nuevos cambios no generen problemas en producción.
Pero… en realidad hay otra opción … ¡podemos implementar Contract Testing!
En realidad Contract Testing es una aproximación muy similar a los test unitarios. La única, e importante diferencia es que vamos a usar Pact para las simulaciones.
Pact es un servidor que funciona a nivel HTTP, por lo que simulará endpoints.
En nuestro caso, lo hará con el end point que tu servicio de facturación necesita del microservicio de usuarios.
Vamos a tener unos test, que llamarán al endpoint que necesitemos, y que definirán las expectaciones, que en nuestro caso puede ser, recibir un código de respuesta 2XX, y que el cuerpo de la respuesta contenga el campo “usuario” que estamos esperando.
Una vez se tienen las expectaciones definidas, el test llamará al código que se quiere probar, que a su vez hará una llamada real al end point del servicio de usuarios del que se depende.
Esta petición llegará al servidor de Pact, que devolverá la respuesta según las expectaciones definidas previamente en los tests.
Otra posibilidad es que el test haya sido definido como “Si llega una petición sin una cabecera determinada, falla”. Por lo tanto en este caso, si el código de tu servicio de facturación realiza la llamada sin esa cabecera, Pact fallará.
El resultado de la ejecución del código del servicio de facturación será finalmente devuelto a los tests, que realizarán las verificaciones necesarias.
¿Qué tiene Pact de diferente con respecto a otro servidor HTTP normal?
En background, mientras el Contract Testing se está ejecutando, el servidor Pact está grabando cada una de las interacciones HTTP.
Todas esas interacciones se almacenan en lo que se llama fichero Pact o contrato.
Ese contrato se puede compartir a lo largo del ecosistema, con los microservicios necesarios, en nuestro caso el servicio de usuarios, y una vez éste lo tenga, podrá repetir cada una de las interacciones grabadas contra su código para verificar que lo que devuelve, es lo que se especifica en el contrato.
¿Cómo se comparte el fichero Pact o el contrato entre servicios?
Existen varias opciones para hacer esto.
Si ambos servicios comparten un sistema de ficheros, se puede almacenar ahí, donde ambos servicios pueden acceder.
Si no es así, puede compartirse vía url, por ejemplo se puede subir el contrato generado al repositorio, y compartirlo vía url una vez esté en remoto. También se podrían user servicios en la nube de Google, AWS o Microsoft Cloud…
Pero también podemos hacer uso de las herramientas que Pact ofrece. En este caso es Pact Broker, que es un servicio que alojará los contratos y ayudará a la hora de ser compartido.
Cuando en el consumidor (nuestro servicio de facturación) se genera un nuevo contrato, éste se publica en Pact Broker y eso permite que los proveedores (servicio de usuarios) lo descarguen cuando lo necesiten.
Este sistema trae bastantes ventajas. Todas las librerías de Pact tienen el servicio Pact Broker accesible. Sólo necesitarás indicar a la librería de Pact donde está hospedado el servicio Pact Broker, y él se hará cargo del resto.
Pact Broker no es un servicio público que se pueda utilizar. Es algo que tendrás que tener en tu sistema, siempre ejecutando y accesible.
¿Solucionaría Contract Testing nuestro problema de integración de microservicios?
Definitivamente sí.
El contrato que tu servicio de facturación habría generado, esperaría como respuesta del servicio de usuarios la propiedad “user”.
Cuando el servicio de usuarios repitiera las requests y verificara si las respuestas reales se corresponden con lo que el contrato determina, llegaríamos a la incongruencia, porque la respuesta real tendría la propiedad “users”.
Si en esa repetición la validación hubiera coincidido, Pact habría dado un resultado satisfactorio, y el servicio de usuarios podría subir a producción.
A diferencia de lo que ocurría con los test unitarios, donde los desarrolladores del servicio de usuarios podían ir y modificar los test para hacer que pasaran, en esta ocasión esto no se puede realizar porque para modificar el contrato, tendría que haber cambios en tu servicio de facturación.
Problemas con el testing en arquitecturas de microservicios
A medida que un sistema con arquitectura de microservicios se hace más y más grande, el mapa de comunicaciones entre ellos se hace inmanejable.
Mucha veces ocurren situaciones en las que los desarrolladores simplemente no saben quienes son sus consumidores.
Es probable que en esta historia, el desarrollador que cambió el servicio de usuarios no supiera que tu servicio de facturación era un consumidor y que además necesitaba de ese campo que iba a ser cambiado.
Siguientes pasos una vez Pact falla en los test
Como ya hemos visto, si Contract Testing hubiera estado implementado, no habrías tenido alarmas de PagerDuty con problemas en producción.
Pero… ¿cuál sería el siguiente paso para los desarrolladores del servicio de usuarios, que realmente querían hacer ese cambio y desplegarlo en producción?
Pues sencillamente, ahora que saben lo que supondría el cambio y a qué servicio rompería, se necesitaría establecer una conversación entre los equipos para ver alternativas.
Se podría desplegar este cambio de distintas formas para no romper. Podría mantenerse la propiedad actual, e incluir una nueva… se puede utilizar versionado de API por parte del servicio de usuarios… o se podría desplegar antes el servicio de facturación, para soportar ambos campos ”usuario” y “usuarios”.
Definición de tests con valores específicos
Hasta ahora hemos estado hablando únicamente de estructura en la respuesta de un servicio. Este es el verdadero objetivo de los test de Contract Testing, y no nos deberíamos meter a probar la funcionalidad.
Pero con Pact también se puede especificar si se desea test que esperen una determinada respuesta. Por ejemplo, podemos querer no sólo que nos devuelva el campo “user”, sino que éste sea “Alex”.
{
“user” : “Alex”
}
Esto requerirá que el proveedor sepa que necesita tener el usuario Alex en la base de datos.
Y es algo que puede indicarse con la propiedad state, con la que se le indica a Pact que para dicho test, el estado del proveedor requerirá el estado en el cual Alex está presente en la base de datos.
{
“state” : “Alex @ DB”
“user” : “Alex”
}
Una vez el contrato se ha generado con estas especificaciones, cuando el proveedor va a repetir la secuencia de interacciones, antes de ejecutar ésta, Pact le va a decir al servicio de usuarios que por favor, se encuentre en el estado en el que Alex está en la base de datos antes de realizar la request.
Sólo cuando esto es así, se ejecutará la request y se verificará si la respuesta real coincide con lo especificado en el contrato.
¿Cómo puede el servicio de usuarios cambiar de estado?
Esto depende de cómo esté orientado dicho servicio. Si es un servicio RESTfull, probablemente se pueda hacer uso del POST para añadir dicho usuario. Si esta opción no está disponible siempre se podrá ejecutar una sentencia SQL/NoSQL para insertar la información directamente en la base de datos.
¿Y si invertimos el orden de los cambios?
Hasta ahora hemos estado hablando de nuestra historia y de todo lo que ese pequeño cambio en el servicio de usuarios supuso en nuestra calidad de vida ese viernes por la tarde.
Vamos a darle la vuelta a la tortilla e imaginarnos ahora que ese cambio no se ha producido y que eres tú en tu servicio de facturación el que quieres hacer el cambio de pasar de “user” a “users”.
Tú eres el consumidor, por lo tanto el que genera el contrato.
Esto quiere decir que te da el poder de arreglar los fallos que vas a encontrar cuando hagas ese cambio en tu código. Porque cuando lo hagas, y corras los test, éstos van a indicarte que hay algo mal.
Así que… la secuencia de acontecimientos podría ser:
- Haces los cambios en el código.
- Pasas los test.
- Fallan.
- Arreglas los test para que funcionen, ya que eres el Consumer y tienes esa posibilidad.
Ahora… ¿puedes desplegar a producción? Si lo haces, volveríamos a tener el mismo problema. Así que esperemos que la respuesta sea que no.
Y efectivamente, la clave es que para ser capaz de desplegar, el contrato que has generado tiene que ser validado por el proveedor, en este caso el servicio de usuarios.
Y como te imaginarás, esto no será así, porque en el escenario en el que nos encontramos, el servicio de usuarios no ha hecho cambio alguno en su código.
¿Cómo validar en el proveedor los nuevos contratos del consumidor?
Lo que necesitamos es algún medio por el que pode ejecutar la validación del contrato en el proveedor.
Esto puede conseguirse de distintas formas.
- Manual
Cada vez que se genere un nuevo contrato, lanzar a mano las validaciones que se necesiten en los proveedores involucrados.
- Ejecución directa
Si el consumidor y el/los proveedores conviven en la misma máquina, y la opción de validar está accesible, siempre que se genere un nuevo contrato, sencillamente se puede ejecutar esta validación de forma automática.
- Utilizar Pact Broker haciendo uso de su webhook
Cuando el consumidor genere y publique el nuevo contrato, Pact Broker estará configurado para lanzar la verificación en ellos proveedores.
Ninguna de estas tres opciones está totalmente automatizada, porque aunque la validación se ejecute en el proveedor, nada frena que el consumidor pueda ser desplegado. Hay que tener un ojo puesto en las validaciones de los proveedores para proceder o no de forma manual con el despliegue.
- Especificación Swagger
Hay una cuarta opción, que aun es más novedosa que el Contract Testing. A día de hoy esta aproximación se ha bautizado como Bi-Directional Contract Testing.
Esta vez nos apoyaremos de la especificación Swagger del API del proveedor. Swagger incluye la especificación de la estructura de las request y response de los endpoint que un API expone.
Se puede utilizar esta especificación Swagger de los proveedores para realizar la comparación en la fase de validación, tanto en la validación del consumidor, como en la del proveedor. Y si el contrato generado coincide con la especificación Swagger, proceder a dar como satisfactoria la verificación y proceder con el despliegue.
Cómo empezar a implementar Contract Testing
En la página oficial pact.io es donde se puede encontrar la mejor información. Una introducción a Contact Testing y algunas guías para seguir paso a paso.
Pact está pensado para trabajar con distintos lenguajes, así que encontrarás distintas guías según el lenguaje que utilices en tu proyecto.
Contract Testing is a team method. De igual forma que con los test de integración y los end-to-end necesitan de una coordinación elevada entre los distintos equipos, la implementación de Contract Testing necesita que el ecosistema crea en ello y lo implemente.
Esta puede ser de hecho una de las labores más complicadas en el proceso. Mucho más que su implementación propiamente dicha.
Pact ha generado algunos documentos exactamente orientados a persuadir. https://docs.pact.io/faq/convinceme. Puedes usarlos si lo consideras necesario porque en esa documentación se pueden encontrar puntos que realmente pueden llegar a convencer sobre la implantación de Contract Testing.
Referencias
Este post ha sido inspirado del siguiente vídeo, que aun siendo de 2017, ha sido de los mejores contenidos que he encontrado con contenido de valor sobre el Contract Testing.