Consumer Driven Contract Tests
En BBVA Labs seguimos la pirámide de tests propuesta por Mike Cohn: tenemos un conjunto grande de test unitarios, fáciles de implementar y que se ejecutan en cada cambio en el código; un conjunto de tests de aceptación que se ejecutan siempre que se haya pasado todos los tests anteriores; y por último, los tests end-to-end que sólo se ejecutan cuando se desea liberar alguna funcionalidad.
La complejidad de la implementación de estos tests van en aumento según se sube en la pirámide. En los tests end-to-end, donde también se prueba la integración de servicios, se requiere levantar la infraestructura, los servicios a los que se desea testear y las servicios con los que se integra. El establecimiento del entorno del testeo, junto con la implementación y ejecución de los tests, es bastante más complejo que los test unitarios.
Adicionalmente al coste de ejecutar estos tests, aparece otro problema cuando un servicio cambia de formato de mensaje. Lo que se desea evitar es un despliegue tipo Big Bang (desplegar el servicio y todos sus dependientes a la vez) debido a este tipo de cambios rompe el despliegue continuo. Por tanto, durante un periodo de tiempo el productor de un servicio tiene que dar soporte a dos versiones del mensaje mientras los consumidores se van actualizando a la nueva, pero los tests del consumidor prueban solo con una única versión del productor del servicio.
Desde BBVA Labs hemos realizado un experimento para reducir el número de servicios a desplegar en los test y para asegurar, en un sistema de despliegue continuo, que la comunicación entre servicios de distintos dominios se mantiene a lo largo del ciclo de vida del producto software. Para este experimento se ha decidido evaluar las actuales herramientas para realizar Consumer Driven Contract testing (CDC testing o simplemente contract testing).
Introducción
La integración de varios módulos de una aplicación mediante APIs es una tarea compleja, no por el hecho de definir la API, sino durante toda la vida de esta API.
Los tests CDC son un tipo de tests que permite verificar la integración entre dos o más servicios permitiendo comprobar que la solución para la evolución de las APIs es válida.
En lugar de realizar un test de integración, CDC divide el test en dos partes que se pueden ejecutar en momentos distintos:
- En el consumidor se ejecuta un test que provoque la llamada al productor del servicio. El productor del servicio es sustituido por un servicio Mock, el cual emula el comportamiento del productor. De estas interacciones con el productor de servicio se genera uno o varios contratos.
- En el productor de servicio se ejecuta otro tipo de test donde se inicia un verificador que ejecuta todos los contratos registrados para el productor. Este verificador replicará las peticiones realizadas por los consumidores definidas en los contratos y verifica que la respuesta del productor cumple el mismo contrato.
Así, el consumidor de un servicio tendrá uno o más contratos por cada servicio consumido y cada servicio implementado debe cumplir con todos y cada uno de los contratos que tenga con cada uno de los consumidores del sistema. De esta forma, si estos tests se ejecutan en cada una de las versiones del productor de servicio, se asegurará siempre que en cada entrega no deje a un consumidor con fallos en la comunicación con el servicio.
Para saber más, se recomienda leer el artículo de Pierre Vincent: Why you should use Consumer-Driven Contracts for Microservice integration tests, o el artículo de ThoughtWorks escrito por Ian Robinson: Consumer-Driven Contracts: A service evolution pattern.
Herramientas para CDC Testing
En la presentación testeando microservicios, de Martin Fowler, muestran tres herramientas para hacer CDC tests:
- Janus es una herramienta que permite especificar los contratos entre servicios en Clojure, facilitando la creación de los tests en el productor. La carencia que tiene Janus es que no facilita la validación del contrato por parte del consumidor utilizando mocks del productor del servicio.
- Pacto es una herramienta de ThoughtWorks implementada en Ruby. Actualmente esta herramienta ha sido abandonada y recomiendan utilizar Pact.
- Pact es una herramienta que permite crear los contratos (denominados pactos) entre los servicios: desde el consumidor permite especificar comportamientos del productor de servicio y registra todas las interacciones durante los tests de integración mediante un mock del productor del servicio; y desde el productor facilita la evaluación de los pactos, emulando las mismas peticiones registradas en estos. Adicionalmente, dispone de varias implementaciones en distintos lenguajes: NodeJS/JavaScript, JVM (Java y Scala), Go, Swift/Objective C, Ruby, .NET.
Para la evaluación, hemos elegido Pact debido a que da soporte tanto al consumidor como al productor del servicio y porque hay soporte para NodeJS y JVM, que son los lenguajes con que hemos decidido realizar las pruebas.
Hemos elegido Pact debido a que da soporte tanto al consumidor como al productor del servicio y porque hay soporte para NodeJS y JVM"
Experimentando con PACT
Pact es una herramienta que facilita hacer CDC testing cuyo canal de comunicación es HTTP y los mensajes están en formato JSON.
Pact está compuesto por tres partes:
- Pact para el consumidor del servicio.
- Pact para el productor del servicio.
- Un servidor para gestionar los pactos.
Pact para el consumidor del servicio
En la parte del consumidor, facilita la creación de los pactos (contratos, según la terminología del CDC testing) utilizando un servidor de mocks donde se van especificando las correspondencias entre el consumidor y el productor del servicio.
Este servidor de mocks registra un conjunto de interacciones para un mock de tal forma que cuando se ejecuten los tests y haya alguna interacción que se ajuste a la petición, este devolverá una respuesta defina en la interacción ejecutada. El servidor va grabando todas las peticiones y respuestas generadas desde el mock para luego poder generar los pactos.
Este servidor de mocks tiene las siguientes funcionalidades:
- Crear un nuevo mock en un puerto.
- Añadir interacciones a un mock.
- Verificar que se ha ejecutado alguna interacción almacenada.
- Borrar las interacciones almacenadas, pero sin llegar a borrar los pactos identificados.
- Parar un mock y generar los pactos identificados hasta ese momento.
Una interacción está compuesta por:
- Una descripción del estado del productor antes del test. Esto es el Given del GivenWhenThen de los tests de integración. Esto será de utilidad a la hora de validar los pactos en el productor del servicio.
- Establecer cuál es el consumidor y cuál es el productor del servicio.
- Una descripción del test.
- Describir la petición y la respuesta de la interacción.
- En la descripción de la petición se puede especificar la ruta HTTP, la query, las cabeceras y el cuerpo de la petición, pudiendo utilizar expresiones regulares para poder describir de una forma más genérica la petición.
- En la respuesta se describe las cabeceras y la respuesta en JSON, pudiendo indicar validaciones extras en el mensaje JSON, como indicar que cumple una expresión regular, que sea de un tipo de datos u otras restricciones.
Debido a que no hemos encontrado en la documentación cómo se usa el servidor de mocks en los tests, y que también hemos encontrado algún ejemplo que está mal aplicado, exponemos aquí el ciclo normal durante los tests para generar los pactos:
- Crear el servidor de mocks.
- Crear tantos servicios mocks como estén implicados en el suite de tests, un servicio por puerto.
- Añadir las interacciones implicadas en el test a ejecutar.
- Ejecutar la parte del consumidor que lleve a ejecutar las peticiones HTTP a los productores de servicios.
- Verificar que se ha ejecutado alguna interacción programada y verificar que el test es válido.
- Limpiar las interacciones para poder ejecutar el siguiente test.
- Ir al paso 3 hasta que se haya ejecutado todas las integraciones posibles con otros servicios.
- Finalizar los mocks, lo que provoca la generación de los pactos.
- Terminar el servidor de mocks.
Pact para el productor del servicio
En la parte del productor, Pact facilita la implementación de los test de verificación de los pactos. Estos tests arrancan el servicio, ejecutan todas las peticiones registradas en los pactos, y por último, verifican que la respuestas coinciden con lo especificado en los pactos.
Hay lenguajes que no tienen soporte completo para ejecutar los tests utilizando los frameworks de tests del lenguaje, lo que provee Pact es de una aplicación implementada en Ruby que lee los contratos a partir de una URI y luego se comunica con los tests del productor mediante peticiones HTTP. Esto último sucede con NodeJS y ya mostraremos más adelante cómo utilizar esta herramienta para implementar los tests de validación de los pactos.
Servidor de pactos: Pact Broker
A la hora de almacenar y gestionar los pactos generados, Pact provee un servidor de pactos, el Pact Broker.
Pact da soporte en algunos lenguajes a poder descargar los últimos pactos por productor. Pero si en un lenguaje o framework de test no están soportados, el Pact Broker posee un API rest para poder navegar por todos los consumidores y productores detectados en todos los pactos, incluido sus versiones, para poder descargar los pactos.
Experimentación con Pact
Hemos diseñado y publicado en nuestro repo público de GitHub un proyecto para evaluar Pact.
Este proyecto de ejemplo consta de dos consumidores (consumer1 y consumer2) y de dos servicios (users y sales):
- El servicio users provee una lista de información de clientes de un sistema de compras. De cada cliente se dan datos como el correo electrónico y su dirección. Este servicio se ha implementado en Scala.
- El servicio de sales provee una lista de las ventas realizadas por cliente en el sistema. En cada elemento se da que compras ha realizado un usuario, que consiste en precio de cada artículo y el tipo del artículo comprado. Este servicio se ha implementado en Scala.
- El consumidor consumer1 es un proceso que ejecuta un informe del dinero gastado por cada cliente y en donde vive el cliente, así se podrá dar un informe de gasto por zona urbana. Este servicio se ha implementado en Scala.
- El consumidor consumer2 es un proceso que ejecuta un informe del tipo de productos en los que está interesado cada cliente y su correo electrónico para poder enviarle a su correo electrónico posibles ofertas de productos similares. Este servicio se ha implementado en NodeJS.
La elección de haber elegido Scala es para probar el soporte que da en este lenguaje, ya que, como alternativa, siempre se puede utilizar el soporte que da sobre Java.
Limitaciones encontradas
En los consumidores:
- Pact da soporte tanto a Node como a Scala/Java, pero con ciertas limitaciones. Hemos encontrado que si un test de consumidor implica llamar a dos o más productores de servicios sólo se puede utilizar Pact para JUnit en caso de implementar sobre JVM. Es decir, el soporte a Scala sólo cubre el caso en que el test dependa de un productor de servicio.
En los productores de servicio:
- El verificador sobre Scala no se conecta al Pact Broker, por tanto, hay que hacer un trabajo extra en el test para navegar por el Pact Broker para buscar los pactos para luego descargarlos.
- El verificador sobre Node descarga un proyecto en Ruby (Pact Provider Verifier). Este se descarga los pactos del Broker o por una URL y luego va comunicándose con el test de verificación mediante HTTP para pedir que ejecute los Given de cada pacto, haciendo así el test en algo engorroso al tener que levantar un servidor HTTP. Adicionalmente se ha comprobado que el motor que ejecuta la validaciones, no implementa todos los casos de matchers, lo que provoca que no siempre puede ejecutar correctamente las validaciones.
Resultados del experimento con Pact
Podemos concluir que Pact da soporte a contract testing, y gracias al Pact Broker, facilita un mecanismos por el que cada productor de servicios puede seleccionar y filtrar qué pactos validar.
En contrapartida, en la versión actual sólo da soporte completo a Java utilizando como framework de test JUnit, tanto en la parte del consumidor como en el productor de servicio. Y en caso de Node, sólo da soporte para la parte del consumidor.
Conclusiones
Con el experimento utilizando Pact se ha podido comprobar que dos servicios se integran sin tener que establecer un test específico de integración. Además habilita la posibilidad de no sólo comprobar la compatibilidad de las últimas versiones de cada servicio, sino de un rango de estas.
Pero como contrapartida, se ha visto que no existe mucha variedad de herramientas para realizar contract testing, y, además, las que existen no dan soporte completo a un amplio número de lenguajes.
Estos resultados nos dejan abiertos los siguientes campos:
- Debido a la falta de soporte a distintos lenguajes, proponemos utilizar un lenguaje comúnmente aceptado para especificar tests (contratos), para que el consumidor especifique que es lo que espera de los productores de servicios y que luego estos productores puedan coger estos tests e implementarlos dentro de su línea de despliegue. Por ahora, el candidato para especificar estos tests sería Gherkin, ya que tiene soporte en los lenguajes mayormente utilizados.
- Una vez establecido como generar los contratos y como verificarlos, hay que ver las distintas estrategias para integrarlos en las líneas de despliegue continuo, evitando lo máximo posible la interdependencia de las líneas de despliegue (evitar un mayor fan-in)
Te animamos a participar enviando tus comentarios a BBVA-Labs@bbva.com y, si eres de BBVA, aportando tus propuestas de publicación.