Climate Disaster

AMIDST warns of global climate change impacts — from extreme weather patterns, to more recent, the introduction of animal-to-human pathogens — Pakistan’s actions have made it a serious crime, if not…

Smartphone

独家优惠奖金 100% 高达 1 BTC + 180 免费旋转




Jugando con Spring Cloud Contract

Mediante un ejemplo sencillo hemos asentado conceptos como consumer, producer, servicio y hemos puesto en evidencia que tan importante como testear las funcionalidades en consumer y producer de forma independiente lo es asegurar que la interacción entre ambos es correcta.

Empezaremos definiendo el acuerdo: escribiremos la especificación que tienen que cumplir consumer y producer para que la comunicación funcione correctamente. En Spring Cloud Contract se utiliza el término contrato. Se puede definir de diferentes formas (groovy, yaml, java, kotlin), nosotros hemos elegido yaml porque para este ejemplo nos parecía que podría resultar fácil de leer.

Para nuestro caso de uso definimos el siguiente contrato:

Este contrato debe estar accesible para el producer. En este caso y por simplificar estará en la carpeta /test/resources/contracts/worklogs del producer.

Una vez que hemos definido el contrato tendremos que hacer la implementación que lo cumpla y los tests necesarios que lo verifiquen. En este ejemplo ya partíamos de la implementación, así que ¡vamos con los test!

Modificamos el pom.xml para añadir la dependencia de Spring Cloud Contract Verifier y el plugin spring-cloud-contract-maven-plugin. Con este último conseguiremos que de forma automática:

Como se muestra en el pom.xml además de las dependencias hemos configurado algunas de las propiedades del plugin:

¿Qué es esto de la clase base de test? Según la especificación debemos generar una clase base que los test autogenerados extenderán. Esta clase debe contener toda la información necesaria para ejecutarlos (por ejemplo, podríamos configurar mocks de algunos beans, popular la base de datos con datos específicos para los tests…).
Para este ejemplo hemos creado una clase base muy sencilla cuya única responsabilidad será levantar el contexto. El contrato lo hemos definido en la carpeta contracts/worklogs, por lo tanto (en base a la documentación que indica que el nombre se infiere en base a los nombres de las dos últimas carpetas), la clase se llama WorklogsBase.java.

Como mencionábamos en el apartado anterior, con este plugin podemos generar automáticamente los tests que aseguren que el producer cumple el conrato. Para ello hacemos ./mvnw clean test y vemos que:

¿Cómo son los tests que se autogeneran? Están en la carpeta generated-test-sources. En este caso en concreto, como tenemos una única carpeta, se genera una única clase:

Como vemos hay un método de test para la definición del contrato. Si tuviésemos más de una, en ese caso tendríamos un test por cada uno de ellos.

En el test que se ha generado vemos cómo se hace un assert para comprobar que el statusCode de la response es el esperado. Además vemos cómo se verifica que el tipo de respuesta sea un json y cómo se ha parseado el fichero .json (al que hacíamos referencia en la especificación) para hacer los assert s necesarios que aseguran que la respuesta es la esperada.

¿Y desde eclipse?

Personalmente utilizo eclipse en mis desarrollos, así que me interesa poder ejecutarlos desde el IDE. Obviamente primero necesitamos que se generen, esto ya hemos visto cómo debemos hacerlo mediante ./mvnw clean test. Pero si no he cambiado el contrato y no hace falta que se regeneren los test y además estoy desarrollando y quiero pasar todos los tests, ¿cómo lo hago? Como son clases autogeneradas, es necesario añadir las carpetas de generated-test-sources al buildpath. Por ejemplo, en este caso:

A diferencia del producer, los tests en el consumer relacionados con el contrato no se generan de forma automática. Pero no estamos solos: recordemos que al mismo tiempo que se han creado los tests del producer también se ha generado un .jar con el stub que nos permitirá simular las llamadas al producer desde los tests del consumer.

En nuestro caso, el jar es: timeReports-producer-0.0.1-SNAPSHOT-stubs.jar que podemos encontrarlo en la carpeta target del producer.

En este caso, al tener ambos proyectos en local, si en lugar de hacer ./mvnw clean test (en el producer) hacemos ./mvnw clean install tendremos dicho jar directamente en nuestro repositorio local de maven, con lo cual, podremos configurar nuestro consumer para que acceda a él.

Para poder tener acceso a él añadimos la siguiente dependencia en el pom.xml:

Además, no debemos olvidarnos de dependencia de Spring Cloud Contract para poder ejecutar el stub añadido.

Una vez añadidas las dependencias ya podemos crear el WireMock basado en ese stub y crear nuestros tests. Nosotros lo haremos mediante la anotación @AutoConfigureStubRunner donde indicamos nuestro stub para que sea automáticamente descargado y registrado en el Wiremock. Un ejemplo podría ser:

Con esto ya tendríamos la comunicación entre ambos testeada, asegurándonos que si en algún momento en alguno de los dos componentes hubiese una modificación en el contrato los tests del otro fallarían. ¿Lo vemos?

Supongamos, que realizamos el mismo cambio que proponíamos en aquel post:

Pasamos los tests y efectivamente fallan: tanto el unitario que tenemos como el autogenerado. ¿Y esto por qué? Porque en realidad hemos modificado el contrato. Actualizamos el contrato y corregimos los tests que fallan:

Hacemos ./mvnw clean test y ahora ya pasan todos nuestros tests. ¡Bien! Vemos cómo ha cambiado el test autogenerado.

Como hemos cambiado el contrato debemos hacer llegar al consumer la actualización, así que hacemos ./mvnw clean install en el producer y pasamos los tests del consumer con ./mvnw clean test.

¿Qué sucede? Si bien los tests unitarios que teníamos siguen pasando correctamente, el nuevo test añadido falla: nos hace ver que algo ha cambiado en el contrato, así que la aplicación no va a funcionar. ¡Bien! Objetivo conseguido: hemos detectado el problema antes del despliegue en producción.

Modificamos la implementación del consumer:

Pasamos los tests, y vemos que al cambiar la implementación los tests unitarios también han dejado de funcionar (lógicamente). Sólo nos quedará por lo tanto, corregirlos.

Como hemos visto en el ejemplo, es importante que cuando cambia el contrato tanto consumer como producer estén al tanto del cambio.

En este caso como es desde el producer desde donde se realiza el cambio, es importante que el consumer reciba la nueva especificación a través del stub (si no, los test seguirán pasando). En nuestro ejemplo es sencillo porque lo tenemos todo en local. Nos sirve para explicar el concepto de forma sencilla pero no nos olvidamos de que no refleja la realidad, donde muchas veces diferentes personas están trabajando en uno o en el otro sin necesidad de tener los proyectos en local.
Existen muchas formas de organizar el código y por lo tanto existen diferentes soluciones, que habría que analizar en función del proyecto y sus necesidades. Las preguntas más importantes a las que habría que dar respuesta sería:

No vamos a entrar a valorar estas y otras muchas cosas, que habría que tener en cuenta a la hora de ponerlo en práctica porque la respuesta será depende. Depende del proyecto, de la organización de los equipos,… En la documentación de Spring Cloud Contract hay diferentes recomendaciones y ejemplos que pueden sernos de utilidad.

Add a comment

Related posts:

How to Recognise a Toxic Work Environment

Between May and October 2016, I worked at a large, international company in the finance department. I was a temporary administrative assistant and got the job through a local recruitment agency. This…

Compile Solidity Smart Contract for AION on a non Ubuntu OS

So if you are a smart contract developer and using other OS like Mac OS , Windows or other Linux flavor, you have to either use web3 or Java RPC call for smart contract compilation. Another option…

Problems Of Design Patterns

In software engineering, creational design patterns are design patterns that deal with object creation mechanisms, trying to create objects in a manner suitable to the situation. The basic form of…