Evaluando Consumer-Driven Contract Testing con Pact
Pact es una librería con soporte en múltiples lenguajes de programación para hacer Consumer-Driven Contract Testing de integraciones vía HTTP y mensajería.
Desde que hace la tira Iván Loire me descubriera en un tuit un poco de rebote su existencia, había vuelto a cruzarse por mi radar en un par de ocasiones la posibilidad de evaluarlo, pero por unas razones u otras terminaba sin priorizarlo y caía en el olvido.
Hace unas semanas Alberto Gualis andaba preparando un taller de Introducción al contract testing con Pact para hacerlo en Bilbao Software Crafters. Él estaba empezando a probarlo para testesar los contratos entre aplicaciones frontends con sus respectivos backends y a raíz de eso estaba preparando el taller.
Ahora que compartimos oficina estuvimos discutiendo sobre ello, y enfrentándolo a otro tipo de soluciones como Dredd para testear los documentos de descripción de APIs, librerías de Record & Replay como VCR o Wiremock para hacer tests de integración con servicios externos guardando peticiones reales, etc. Así que al final me lió para echarle un cable para hablar de ese tipo de herramientas en el taller y complementar un poco más el contenido.
Esta fue la excusa perfecta para forzarme a hacer un spike para entenderlo y poder evaluarlo mejor. Hace unos pocos meses ya estuve pensando en darle un tiento por ver si tenía sentido introducirlo en Devengo para enterarnos rápido de si algún cambio entre el API y las apps móviles rompe con el contrato esperado.
¿Cómo funciona Pact?
La visión de Consumer-Driven Contract Testing de Pact es que los clientes o consumers son quienes generan el contrato, y luego el servidor o provider deberá comprobar si es capaz cumplir con él.
El flujo simplificando un poco es el siguiente:
- Escribir los tests de integración del consumer usando alguna de las librerías de Pact para hacer stubs de las respuestas que se esperarían del provider.
- Al lanzar los tests, si pasan en verde se genera un fichero JSON que describe el contrato esperado por el consumer. He dejado un ejemplo sencillote en gist.
- En otro momento el provider lanza con Pact la verificación. Busca que pueda cumplir con lo descrito en ese JSON simulando las peticiones que haría el consumer y comprueba que puede responder lo esperado.
Al lanzar los tests de forma independiente sobre cada artefacto frente a hacerlo end to end tenemos un feedback más rápido, nos facilita el mantenimiento de esos tests y nos da mayor estabilidad. Todo esto sin perder confianza de que hemos roto el contrato entre los artefactos.
Integración con los pipelines de CI
Evidentemente si optamos por una solución así querremos integrarla en nuestro pipeline de integración continua.
Normalmente no sólo querremos que se validen los contratos cuando se suba un cambio al repositorio del provider, si no que si cambia el fichero que representa al contrato de un consumer queremos desde el provider se compruebe que se sigue cumpliendo ese contrato (o no).
Al generarse ficheros JSON planos no suena descabellado orquestar con un puñado de scripts y hooks estas comprobaciones, pero ya hay un proyecto dentro del paraguas de la fundación Pact que nos ayuda a resolver eso: Pact Broker.
En Pact Broker se guardan los contratos de los consumers y es donde los providers irán a validarlos, nos facilita el lanzamiento de webhooks y el trabajar con distintas versiones de los contratos.
Conclusiones por el momento
Sólo he trabajado con la parte de HTTP, no he hecho ninguna prueba con el soporte de mensajería.
Para mis pruebas he usado la gema de ruby tanto para el provider como para un consumer, además de la librería de JVM para otro consumer. Inicialmente me parece que deja un pelín verboso la preparación de los tests en ambos lenguajes, pero no he hecho aún el ejercicio de intentar esconder eso en algún tipo de helpers que mejorarían la legibilidad de los tests.
El DSL de la librería de JVM me pareció que dejaba código difícil de seguir para ver las estructuras de respuesta esperadas, luego vi que existe el Lambda DSL que mejora el asunto. Esto es porque aún habiendo buena y abundante documentación y ejemplos, algunos resultaron estar algo desfasados.
La integración de Pact Broker en nuestro pipeline de CI con Travis resultó bastante sencilla, para evitarme montar infraestructura propia lo hice con Pactflow que es un Pact Broker en SaaS. Para publicar o validar con Pact Broker usé la gema pact_broker-client y el plugin de gradle. En el travis.yml
tocó añadir la publicación de los contratos de los consumers y la validación de los contratos en el provider; mientras que en pactflow configué los webhooks que disparan la build de travis del provider cuando el contrato cambia.
Aunque ofrece un flujo donde el consumidor puede empezar a trabajar sin que el proveedor esté disponible, el consumidor debería tener buena comunicación e influencia sobre lo que publica el proveedor. Claramente tanto para servicios externos como equipos que trabajen aislados es una herramienta que no aportará valor.
Y definitivamente creo que cuadra genial con una aproximación API first, acordando el equipo del proveedor con los de los consumidores cómo se va a hacer esa integración para poder empezar a trabajar en paralelo, asegurándonos así que estamos cumpliendo con lo que hemos acordado. Así que inicialmente pinta genial, pero está por ver cómo afecta al flujo del trabajo en el día a día y del contexto de cada uno.