La manera más sencilla de comenzar con contract testing bi-direccional
30 de mayo de 2022
¿Qué vamos a ver en este artículo?
Hasta ahora, el framework de contract testing PACT, se había caracterizado por seguir un enfoque donde la parte consumidora se encargaba de generar los contratos y, por tanto, iniciaba el flujo de trabajo. Por este motivo, a esta aproximación se la conoce como Consumer Driven Contract Testing (CDCT).
En sistemas distribuidos, donde las comunicaciones entre aplicaciones son la clave del correcto funcionamiento, de manera habitual, los consumidores son el eslabón más débil de la cadena, puesto que están a expensas de los cambios realizados en las APIs. Es por ello, que este flujo de trabajo tiene claras ventajas, ya que estaremos asegurando desde el primer momento que no habrá incompatibilidades entre aplicaciones y que la parte proveedora no podrá efectuar cambios que afecten al funcionamiento de los consumidores.
Sin embargo, la implementación de este enfoque puede resultar complejo en determinados contextos. Algunos ejemplos podrían ser:
- La parte consumidora no tiene suficiente detalle técnico sobre la implementación del proveedor
- La comunicación en equipos no es del todo fluida
- La parte proveedora no verifica los contratos generados por el consumidor
- El flujo de trabajo y organización está muy centrada en la parte proveedora y los consumidores deben adaptarse a los cambios realizados
Con el objetivo de solventar este tipo de situaciones nace el contract testing bi-direccional (BDCT) y en este artículo, veremos cómo hacerlo cuando solamente tenemos unos test realizados en Postman para probar la parte proveedora.
¿Por qué es interesante?
Como se ha mencionado anteriormente, la técnica de Consumer Driven Contract Testing presenta importantes ventajas y, realmente, sería la opción más recomendable en entornos donde exista una buena coordinación entre equipos. No obstante, este no siempre es el caso.
Por otro lado, se dan situaciones, donde la parte proveedora ya dispone de una especificación Open API generada o bien ya tiene desarrollada una batería de pruebas del API perfectamente válida y, por tanto, introducir la verificación de contratos en su código, puede resultar redundante.
En estos casos, el enfoque de contract testing bi-direccional, otorga gran versatilidad a los equipos para que sigan el flujo de trabajo más conveniente.
También, cabe destacar que ambas metodologías no son excluyentes entre sí, es decir, el mismo proveedor podrá aplicar las diferentes técnicas con sus correspondientes consumidores.
La ventaja de la implementación realizada por Pactflow es que la introducción de contract testing en los proyectos requiere poco esfuerzo, puesto que la comprobación de la compatibilidad entre los sistemas queda delegada en Pactflow. Entonces, únicamente es necesario que ambas partes publiquen sus esquemas de comunicación.
¿Dónde lo puedes aplicar?
Casos de uso habituales
Seguramente, los casos en los que tenga más sentido implementar el contract testing bi-direccional sea en proyectos ya iniciados donde se disponga de pruebas de integración automatizadas o bien la definición del API se encuentre en formato OpenAPI. En este tipo de situaciones, comenzar con un enfoque consumer driven podría resultar costoso en esfuerzo o incluso redundante.
Otro caso habitual que podemos encontrar viene dado por empresas o equipos cuya organización se centra en la parte productora, obligando a los consumidores a adaptarse a los cambios en la API. En estas situaciones, es posible que resulte complicado realizar los cambios necesarios en los flujos de trabajo, o en el código fuente de las APIs, que permitan la verificación de los contratos. Así pues, nos limitaremos a añadir las acciones de publicación de especificaciones en Pactflow que permitan comprobar que ambos sistemas son compatibles.
Para conocer todos los casos de uso donde se puede aplicar el contract testing bi-direccional puedes consultar esta web.
Dónde no es recomendable
No podemos desaconsejar su uso en casi ningún contexto, si bien es cierto que si la comunicación entre los equipos es fluida y se está comenzado con el desarrollo de los sistemas, el enfoque de Consumer Driven puede tener más ventajas, puesto que se favorecería la detección temprana de errores y la sincronización entre equipos.
¿Cómo funciona?
El contract testing bi-direccional es un tipo de prueba de contratos estáticos donde 2 contratos, uno representando las expectativas del consumidor y otro las capacidades del proveedor, son comparados para garantizar que son compatibles.
Los equipos generarán un contrato del consumidor a partir de una herramienta de mockeo (Pact, Wiremock o similar) y los proveedores del API verificarán un contrato del proveedor (un OAS) utilizando una herramienta de pruebas funcionales para APIs (Postman, por ejemplo). A continuación, Pactflow compara estáticamente los contratos, hasta el nivel de campo, para garantizar que siguen siendo compatibles.
El contract testing bi-direccional (BDCT) ofrece la posibilidad de que cada equipo continúe utilizando las herramientas de testing actuales, con generar un contrato en la parte consumidora y una especificación OpenAPI en la parte proveedora será suficiente, puesto que PactFlow se encargará de realizar las comprobaciones y comunicar los resultados de manera automática.
Esto simplifica la adopción y reduce el tiempo de implementación del contract testing en el flujo de trabajo.
Ejemplo práctico
En este caso, vamos a explicar el proceso a seguir cuando queramos incluir contract testing bi-direccional entre dos sistemas teniendo en cuenta que disponemos de una colección de Postman de pruebas para el proveedor.
Pasos previos
Todo el código de ejemplo puede encontrarse en este repositorio.
Cuenta en Pactflow
Necesitaremos tener una cuenta creada en Pactflow para poder subir los contratos y que sean validados correctamente.
API Token de Pactflow
Una vez tengamos la cuenta, generamos un API Token para poder efectuar las peticiones a Pactflow via HTTP. Para ello:
- Ir a Settings
- Pulsar sobre el menú lateral API Tokens
- Generar un token de tipo Read/write token (CI)
Variables de entorno
Por último, tendremos que exportar las dos variables de entorno de Pactflow para poder usarlas en nuestros scripts:
- PACT_BROKER_TOKEN: con el token generado anteriormente
- PACT_BROKER_BASE_URL: con el nombre de dominio que se haya generado al crear la cuenta en Pactflow. Por ejemplo: https://testdemo.pactflow.io
Requisitos de software
Para este ejemplo, necesitaremos tener en nuestro equipo:
Pasos a seguir desde el lado del proveedor
Desde la parte del proveedor, deberemos publicar en Pactflow la especificación del servicio en formato OpenAPI.
Los pasos a seguir son los que se muestran en la siguiente imagen.
Generar la especificación Open API (OAS)
En nuestro caso, disponemos de una colección de Postman que contiene peticiones que prueban el API del proveedor y mediante la cual vamos a generar nuestro OAS.
Para ello, necesitaremos usar una herramienta que nos haga esa conversión. En este caso, vamos a utilizar postman2openapi , cuyo binario tenemos incluido en el repositorio, junto con el siguiente script:
#!/bin/bash
echo -e "\\n\033[34m ==========> Convert Postman collection into OAS <========== \n\033[0m"
./bin/postman2openapi ./provider/students.postman_collection.json > ./provider/oas.yaml
y ejecutaremos el siguiente comando desde el directorio base del proyecto.
./scripts/postmanToOpenAPI.sh
Como resultado de la ejecución del script obtendremos el fichero oas.yaml con la especificación del API en formato OpenApi que quedará guardado en la carpeta provider.
Publicar el contrato del proveedor en Pactflow
Una vez tenemos generado nuestro OAS, lo publicaremos en Pactflow usando la Pact CLI. Para ello disponemos del siguiente script:
#!/bin/bash
if [ "${1}" == "" ]; then
echo "Please specify PACTICIPANT. Example: ./publish.sh provider-postman"
echo "By default, version will be picked from last commit id. If you want to define a specific version please use: ./publish.sh provider-postman [VERSION]"
exit 1
fi
PACTICIPANT="${1}"
VERSION="${2}"
if [ "$VERSION" == "" ]; then
VERSION=$(git rev-parse HEAD)
fi
OAS=$(< provider/oas.yaml base64)
if [ "$OAS" == "" ]; then
OAS=$(< oas.yaml base64)
fi
BRANCH=$(git name-rev --name-only HEAD)
echo -e "\\n\033[34m ==========> Uploading OAS Provider to Pactflow <==========\n\033[0m"
curl \
--fail-with-body \
-X PUT \
-H "Authorization: Bearer ${PACT_BROKER_TOKEN}" \
-H "Content-Type: application/json" \
"${PACT_BROKER_BASE_URL}/contracts/provider/${PACTICIPANT}/version/${VERSION}" \
-d '{
"content": "'"$OAS"'",
"contractType": "oas",
"contentType": "application/yaml",
"verificationResults": {
"success": true,
"content": "'"$OAS"'",
"contentType": "application/json",
"verifier": "postman"
}
}' || exit 1
echo -e "\\n\033[34m ==========> Tagging ""$PACTICIPANT"" contract with current BRANCH <========== \n\033[0m"
docker run --rm \
-e PACT_BROKER_BASE_URL \
-e PACT_BROKER_TOKEN \
pactfoundation/pact-cli:latest \
broker create-version-tag \
--pacticipant "${PACTICIPANT}" \
--version "${VERSION}" \
--tag "${BRANCH}"
Y ejecutaremos el siguiente comando desde el directorio base del proyecto.
./scripts/publish-provider.sh provider
Si la ejecución finaliza correctamente, tendremos el contrato publicado en Pactlow y será similar a la imagen que podemos ver a continuación.
Comprobar la viabilidad del despliegue del proveedor (can-i-deploy)
Con el comando can-i-deployobtenemos una respuesta inmediata de si es seguro liberar una versión de una aplicación a un entorno específico.
Para ello, al igual que en el paso anterior usaremos la Pact CLI junto con el siguiente script:
#!/bin/bash
echo
if [ "${1}" == "" ] || [ "${2}" == "" ]; then
echo "Please specify PACTICIPANT and ENVIRONMENT. Example: ./can-i-deploy.sh provider-poc production"
exit 1
fi
PACTICIPANT="${1}"
ENVIRONMENT="${2}"
echo -e "\\n\033[34m ==========> Check viability of the ""$PACTICIPANT"" deployment (can-i-deploy) <==========\n\033[0m"
docker run --rm \
--platform=linux/amd64 \
-e PACT_BROKER_BASE_URL \
-e PACT_BROKER_TOKEN \
pactfoundation/pact-cli:latest \
broker can-i-deploy \
--pacticipant "$PACTICIPANT" \
--to-environment "$ENVIRONMENT" \
--latest
Y para ejecutarlo, lanzaremos el siguiente comando desde el directorio base de nuestro proyecto:
./scripts/can-i-deploy.sh provider production
Siendo provider el PACTICIPANT y production el ENVIRONMENT en nuestro caso de ejecución.
Versionar el despliegue del proveedor
Si el can-i-deploy devuelve una respuesta exitosa, podremos desplegar nuestra aplicación y podremos notificar a Pactflow de la release desplegada siguiendo la regla de oro del taggeo.
Pact recomienda poner la propiedad de rama (branch) cuando publiquemos pactos y verifiquemos resultados y usar record-deployment o record-release cuando despleguemos/versionemos.
Para llevar a cabo el versionado del deployment, usaremos la Pact CLI junto con el siguiente el script:
#!/bin/bash
echo
if [ "${1}" == "" ] || [ "${2}" == "" ]; then
echo "Please specify PACTICIPANT, ENVIRONMENT. Example: ./record-deployment.sh provider-postman production"
echo "By default, version will be picked from last commit id. If you want to define a specific version please use: ./record-deployment.sh provider-postman production [VERSION]"
exit 1
fi
PACTICIPANT="${1}"
ENVIRONMENT="${2}"
VERSION="${3}"
if [ "$VERSION" == "" ];
then
VERSION=$(git rev-parse HEAD)
fi
echo -e "\\n\033[34m ==========> Record deployment of new ""$PACTICIPANT"" version in a new environment <==========\n\033[0m"
docker run --rm \
--platform=linux/amd64 \
-e PACT_BROKER_BASE_URL \
-e PACT_BROKER_TOKEN \
pactfoundation/pact-cli:latest \
broker record-deployment \
--pacticipant "$PACTICIPANT" \
--environment "$ENVIRONMENT" \
--version "$VERSION"
Si quisiéramos versionar la release, tendríamos que cambiar en el script record-deployment por record-release en el comando que arranca el docker con la Pact CLI.
Y para ejecutarlo, lanzaremos el siguiente comando desde el directorio base del proyecto:
./scripts/record-deployment.sh provider production
Como resultado de la ejecución del script en Pactflow veremos lo siguiente:
Pasos a seguir desde el lado del consumidor
Los pasos que nos describe Pactflow para la parte del consumidor son los mostrados en la siguiente imagen.
Si no vamos a usar Pact, tendremos que elegir una herramienta de la que podamos extraer la información de los mocks o usar un adaptador preexistente para convertirla en un archivo pact (contrato).
Algunas herramientas tienen opciones para serializar sus mocks a un archivo, y otras requerirán que se realice una introspección a través de sus APIs. Debemos tener esto en cuenta de antemano.
Escribir los tests del consumidor y generar el contrato
Una vez elegida la herramienta, en nuestro caso vamos a usar Pact, debemos implementar los tests del lado del consumidor. Es importante tener en cuenta el comportamiento de la API que el sistema espera para asegurarnos que tenemos la cobertura que necesitamos.
Los test de contrato del consumidor están implementados en el fichero ConsumerContractTest.java y para generar el contrato debemos ejecutar el siguiente comando desde el directorio base del proyecto:
cd consumer && mvn verify
Como resultado del comando anterior, obtendremos el contrato de la parte consumidora que estará alojado en el directorio consumer/target/pacts.
Publicar el contrato del consumidor en Pactflow
Para publicar el contrato, tenemos varias opciones, en nuestro caso usaremos la PactCLI mediante una imagen docker junto con el siguiente script:
#!/bin/bash
echo
if [ "${1}" == "" ]; then
echo "Last git commit id will be used as contract version, if you want to define a specific version please use: ./publish-consumer.sh [VERSION]"
echo
fi
VERSION="${1}"
if [ "$VERSION" == "" ]; then
VERSION=$(git rev-parse HEAD)
fi
BRANCH=$(git name-rev --name-only HEAD)
echo -e "\\n\033[34m ==========> Publish Consumer contract <==========\n\033[0m"
docker run --rm \
-w /pacts-container \
-v "${PWD}"/consumer/target/pacts:/pacts-container \
-e PACT_BROKER_BASE_URL \
-e PACT_BROKER_TOKEN \
pactfoundation/pact-cli:latest \
publish \
. \
--consumer-app-version "$VERSION" \
--branch="$BRANCH" \
--tag="$BRANCH"
Y para ejecutarlo, lanzaremos el siguiente comando desde el directorio base de nuestro proyecto.
./scripts/publish-consumer.sh
Si la ejecución finaliza correctamente, tendremos el contrato publicado en Pactlow.
Comprobar la viabilidad del despliegue del consumidor (can-i-deploy)
Con el comando can-i-deployobtenemos una respuesta inmediata de si es seguro liberar una versión de una aplicación a un entorno específico.
Para ello, usaremos el mismo script que ejecutamos en el paso del proveedor pero en esta ocasión el PACTICIPANT será consumer y el ENVIRONMENT será production.
./scripts/can-i-deploy.sh consumer production
Versionar el despliegue del consumidor
Al igual que en el caso del proveedor, si el can-i-deploy devuelve una respuesta exitosa, podremos desplegar nuestra aplicación y podremos notificar a Pactflow de la release desplegada siguiendo la regla de oro del taggeo.
El script es el mismo que ejecutamos en el paso del proveedor pero en este caso el PACTICIPANT será consumer y el ENVIRONMENT será production.
./scripts/record-deployment.sh consumer production
Como resultado de la ejecución del script en Pactflow veremos lo siguiente:
Pipeline
Para integrar todos los pasos descritos anteriormente en un pipeline Jenkins, hemos creado un Jenkinsfile con diferentes stages en los que vamos llamando a cada uno de los scripts que realizan los diferentes pasos.
#!/usr/bin/env groovy
/* groovylint-disable-next-line CompileStatic */
pipeline {
agent any
environment {
PACT_BROKER_BASE_URL = '<pact_broker_url>'
PACT_BROKER_TOKEN = '<pact_broker_token>'
}
stages {
stage('Get project') {
steps {
/* groovylint-disable-next-line LineLength */
git branch: 'main', url: 'git@gitlab.sngular.com:sngulartech/bi-direccional_contract_testing.git'
}
}
stage('Generate OAS') {
steps {
sh './scripts/postmanToOpenAPI.sh'
}
}
stage('Publish provider contract to Pactlow') {
steps {
sh './scripts/publish-provider.sh provider-postman'
}
}
stage('Can-i-deploy provider') {
steps {
sh './scripts/can-i-deploy.sh provider-postman production'
}
}
stage('Record deployment provider') {
steps {
sh './scripts/record-deployment.sh provider-postman production'
}
}
stage('Generate Consumer contract') {
steps {
dir('consumer') {
sh 'mvn verify'
}
}
}
stage('Publish consumer contract to Pactlow') {
steps {
sh './scripts/publish-consumer.sh'
}
}
stage('Can-i-deploy consumer') {
steps {
sh './scripts/can-i-deploy.sh consumer production'
}
}
stage('Record deployment consumer') {
steps {
sh './scripts/record-deployment.sh consumer production'
}
}
}
}
Habría otras opciones de configuración, como por ejemplo, tener 2 pipelines (uno para los pasos de la parte proveedora y otro para los de la parte consumidora) con webhooks para avisar a cada una de las partes cuando se realiza algún cambio en los contratos. Para más información sobre esta configuración podéis consultar este artículo.
Nuestras conclusiones
Esta nueva funcionalidad de Pactflow facilita enormemente la inclusión del contract testing dentro de los flujos de desarrollo, puesto que otorga una gran versatilidad y, realmente, facilita la transición de los equipos a este modelo de trabajo.
La curva de entrada al contract testing con el enfoque bi-direccional es poco pronunciada y permite re-aprovechar la mayor parte del trabajo realizado en tareas de automatización de pruebas. Por ello, consideramos que es fácil comenzar a implementarlo tanto en proyectos nuevos como en los que ya están en curso.
Repositorio de código
Todo el código de ejemplo puede encontrarse en este repositorio.