Contract testing con spring

0
760

En este tutorial vamos a explorar como hacer contract testing con spring, más concretamente con el proyecto Spring Cloud que tiene el subproyecto spring cloud contract. Contract testing puede ser algo desconocido, pero ciertamente útil en el contexto de arquitecturas complejas u orientadas a microservicios.

Índice de contenidos

1. Introducción

El testing es una disciplina muy importante dentro del desarrollo de software. Dentro de esta disciplina tenemos muchas formas de testear nuestra aplicación y una de ellas es contract testing. Tenemos tutoriales interesantes como este donde se explica como hacerlo con pact y springboot o este donde se explica como hacerlo en tecnologías .NET.

Cuando hablamos de contract testing hablamos de formalizar un contrato entre servicios, para que tanto los servicios que proveen datos como los que lo consumen tengan una base sólida sobre la que desarrollarse. En este tutorial veremos como desarrollarlos con spring.

2. Entorno

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil Lenovo t480 (1.80 GHz intel i7-8550U, 32GB DDR4)
  • Sistema Operativo: Linux Mint 20.3 Una base: Ubuntu 20.04 focal Kernel: 5.15.0-33-generic x86_64 bits
  • Entorno de desarrollo: IntelliJ IDEA 2022.1.3 (Ultimate Edition)
  • Apache Maven 3.8.4
  • Java version: 17.0.3, vendor: Eclipse Adoptium

3. Contract testing

Lo primero seria saber donde encaja todo esto. Si miramos la pirámide de testing:

Imagen de la pirámide de testing
Imagen de la pirámide de testing

Vemos que el contract testing estaría entre los tests de integración y los end to end. Y recordemos, la pirámide debe ser una pirámide.

Dentro del contract testing tenemos diferentes variantes según el enfoque:

  • Consumer Driven Contract Testing: Donde el consumidor escribe el contrato y le dice al proveedor que lo implemente y valide.
  • Provider Driven Contract Testing: Donde el proveedor escribe el contrato y el consumidor prueba su implementación contra ese contrato.
  • Design First Contract Testing: Donde el diseñador del API crea un contrato y tanto el consumidor como el proveedor prueban la implementación contra ese contrato.

4. Porque necesitamos contract testing

Hoy en día son bastante comunes las arquitecturas de microservicios, con sus beneficios e inconvenientes.

En cuanto tenemos interacciones entre nuestros sistemas (servicios que dependen unos de otros), deberíamos tener una forma de probar que esas interacciones aunque cambien, no son un problema y, a ser posible, de forma automática. En este tipo de arquitecturas este tipo de testing se vuelve más relevante (o alguna forma similar de asegurarnos que no rompemos interacciones entre servicios). No cabe duda que tenerlo todo bien testeado es algo fundamental.

En este tipo de sistemas, se tienen productores (lo llamaremos ahora así porque spring los denomina producer no provider) y consumidores. Dependiendo del momento un servicio puede tener un rol u otro. El productor da una determinada funcionalidad que es consumida por otros servicios que serian los consumidores. A la hora de desarrollar los sistemas tanto consumidores como productores, es recomendable tener tests de que las integraciones entre los consumidores y los productores son correctas.

Dependiendo del entorno donde estemos, el consumidor y el productor serán desarrollados por equipos diferentes, o, llegado el momento seguramente nos gustaría tener equipos autónomos para desarrollar o modificar los productores y los consumidores con el menor riesgo posible de romper la API. No nos gustaría tocar algo del API, desplegar a producción y que de repente todos nuestros consumidores dejaran de funcionar como esperábamos.

Muchas veces nos ponemos a desarrollar y en medio del desarrollo se nos olvida que estamos en un contexto mucho más grande, pensamos que no vamos a afectar a nadie, pero acabamos haciéndolo.

Para hacer este tipo de tests, tenemos los siguientes enfoques:

  • Con dobles de otros servicios y realizando tests de integración.
  • Lo despliego todo y hago tests end to end.

Son enfoques distintos y cada uno tiene cabida dependiendo de la situación

4.1. Usando dobles

Lo bueno de usar dobles es que tenemos un feedback muy rápido por qué estamos realizando tests de integración donde las partes que potencialmente son más lentas (las interacciones) están siendo sustituidas por dobles.

Otra ventaja es que los podemos depurar dentro de nuestro entorno de desarrollo.

Por último, si utilizamos dobles, no necesitamos infraestructura para realizar los tests.

Lo malo de usar dobles es que son dobles que hemos hecho nosotros, y puede que no tengamos la confianza de conocer el sistema que estamos simulando con esos dobles. Esto puede desembocar en que el productor cambie el API, pero nosotros no nos enteremos, no cambiemos los dobles y todo se rompa sin que nosotros seamos conscientes de ello porque los tests pasan.

4.2. Usando tests end to end

En este enfoque lo que hacemos es desplegar todo y probar las interacciones.

Como todo es similar a producción, no tenemos el problema de tener los dobles desactualizados y no darnos cuenta de que lo hemos roto, ya que nuestro servicio está interactuado con servicios reales.

Esto tiene algunas cosas que pueden llevarlo a ser impracticable:

  • Puede que tener toda una infraestructura en local no sea viable.
  • Estos tests son muy lentos y el feedback llega muy tarde.
  • Son muy difíciles de depurar, porque implican red (con sus timeouts) y dependencias con otros servicios (delays, servicios que no arrancan).
  • No se puede probar hasta que está todo desarrollado, porque lo estamos integrando.

Hay situaciones que hacen esto inviable, como por ejemplo;

Imagen con un grafo de dependencias entre servicios muy grande de las compañías Hail-o Netflix y twitter
Imagen con un grafo de dependencias entre servicios muy grande de las compañías Hail-o Netflix y twitter

Vemos que la cantidad de interacciones entre servicios es enorme, y no podemos levantarlos todos en local.

4.3. Recapitulando los enfoques

Tenemos una aplicación, unos pocos servicios que colaboran entre sí siendo consumidores o productores y tenemos que ver que esos servicios se integran entre sí, que no rompemos el API cuando la modificamos y que el uso que hacemos del API es el correcto. Los dobles tienen sus problemas y desplegar todo también.

Además de todo esto, queremos equipos independientes, sacando versiones de manera independiente para poder evolucionar los servicios de forma paralela. Según hemos visto estas integraciones entre servicios pueden fallar.

Entonces:

  • ¿Cómo probar un sistema con múltiples dependencias entre sus partes?
  • ¿Cómo permitir que esos tests se ejecuten tan rápido como sea posible?
  • ¿Cómo permitir que equipos distintos desplieguen sus servicios con independencia de los clientes (consumidores) de dichos servicios?
  • ¿Cómo evitar que cambios en la API rompan a los clientes?

Pues si el título es contract testing, efectivamente, con contract testing.

La idea del contract testing es tener los beneficios de usar dobles (tests rápidos y deterministas, evitando que tests end to end fallen porque un servicio no levante por ejemplo) con un entorno fácil de desplegar(como si fueran unitarios o de integración) y la confianza de usar tests end to end en un entorno real.

El contract testing se basa en usar dobles para representar el contrato acordado entre los servicios consumidor y productor. Este contrato será usado tanto en el consumidor como en el productor. Cunado en el consumidor se hacen tests, en vez de usar el productor real, se utilizan los dobles que se ha generado a partir del contrato.

Además, estos mismos dobles que representan el contrato se pueden utilizar también como un seguro de que el productor está respetando el contrato, es decir, en el productor, esos dobles se pueden utilizar como tests para verificar que el productor se comporta como debe, es decir, acorde al contrato.

Los contratos son la definición de patrones petición/respuesta acordados entre el equipo que hace los consumidores y el equipo que implementa el productor. Estos contratos se utilizan para generar dobles y tests de aceptación del productor. Si lanzamos al productor las peticiones que se especifican en el contrato y el productor devuelve respuestas diferentes a las que están en el contrato, el productor no está cumpliendo el contrato. En el cliente las interacciones con el productor se ejecutan contra un doble que tiene las respuestas predefinidas (el contrato en si).

En contract testing tenemos cuatro partes fundamentales:

  • Consumidor: Cualquier servicio que interactúa con otro pidiendo información (El que consume el api)
  • Productor: Cualquier servicio que interactúa con otro dando información (El que provee la api)
  • Contratos: Un conjunto de interacciones representadas por peticiones y respuestas entre el cliente y productor
  • Broker: Un almacén donde guardar los contratos para que puedan ser utilizados como tests

5. Spring cloud contract

El contract testing tiene dos actores, el productor y consumidor. Vamos a verlo desde el punto de vista del consumidor, que suele ser más habitual (Consumer Driven Contract Testing).

Los tests que se hacen en el consumidor, tienen un doble del productor. El contrato se define por separado en el productor y a partir de este contrato el productor genera un doble que se publica con las respuestas definidas en el contrato.

Los tests que se hacen en el productor, simulan el cliente. Las peticiones enviadas son las definidas en el contrato generado, por lo que no hay que inventar ningún test, están ya consensuados entre productor y consumidor

Centrándonos más en spring, tiene un proyecto para ayudar a los desarrolladores a definir contratos y usarlos en los tests de consumidor y productor. Este proyecto se encuentra dentro del proyecto spring cloud, ya que se utiliza mucho con el patrón microservicios en entornos cloud, pero podemos usarlo en cualquier lado como veremos a continuación, simplemente importando las dependencias necesarias. Se está haciendo referencia a apis rest por ser lo mas habitual, pero hay que tener en cuenta que se puede utilizar con apis de mensajería y grpc, es decir, que cualquier sistema que usemos para comunicar servicios es susceptible de que definamos un contrato para que todos los servicios estén alineados.

En concreto, dentro de spring cloud el subproyecto es spring cloud contract Spring contract cloud nos va a proporcionar un DSL para definir los contratos en groovy, YAML, java o kotlin.

A partir de estos contratos generados por el productor (en el dsl elegido) se genera:

  • Un doble que puede usarse para simular el productor en los tests del cliente. El doble devuelve las respuestas configuradas en el contrato.
  • Los tests de aceptación para el productor a partir del contrato.
  • El doble se publica en algún repositorio (maven, github, pact) que actúa como broker.

En el consumidor:

  • El test se implementa manualmente.
  • Se referencia el doble según el broker que tengamos para simular el productor.

6. Ejemplo práctico

6.1. productor

Los contratos pueden estar escritos en groovy, java, yml y kotlin. En nuestro caso los vamos a implementar en groovy. Si optamos por kotlin hay añadir más dependencias. Vamos a tener dos contratos que interactúan con el cuerpo humano, uno que nos dice como está el corazón, y otro que nos pone un corazón nuevo.

Este contrato nos da el corazón. Es una llamada get a /body donde le pasamos un query param que es part donde se le indica que es el corazón (heart) y este nos va a devolver que el status de ese corazón es on.

Luego tenemos este otro contrato que es un put al mismo path (/body) que tiene un body donde le indicamos que le queremos poner y de que tipo será. Esta llamada nos da un status code de 200 con un body que nos da un mensaje de success y en los headers el sitio donde esta esa parte del cuerpo.

Vale, ya tenemos dos contratos en el productor, aparte de eso necesitamos ponerle las siguientes dependencias.

Para que todo funcione, necesitamos el plugin de spring-cloud-contract-maven-plugin donde le vamos a decir que usamos Junit 5 y la clase base que vamos a usar para generar los dobles.

Por si resulta raro, aquí estamos solo usandolo, ya que el proyecto padre es el que las declara al estar en un proyecto multimodulo. En el padre están así:

Si nos hemos fijado cuando declaramos el plugin, tenemos una clase es.dionisiocortes.contractTesting.producer.BaseClass que lo que nos hace es definir los tests que se van a generar de forma automática para el contrato. Esa clase es la siguiente:

El controlador no tiene nada especial, es simplemente un controlador de Spring que tiene el método get para cuando se invoca el endpoint para el get y luego el endpoint para hacer el put:

Y luego la request es un record:

Si ejecutamos

Veremos que ha generado e instalado en el repositorio .m2 el contrato y que además ha ejecutado y pasado dos tests:

6.2. Consumidor

En el consumidor necesitamos dos dependencias que son el spring-boot-test y el spring-cloud-starter-contract-stub-runner:

El equipo que desarrolla el consumidor puede hacer el desarrollo del controlador que hace uso del servicio externo, por ejemplo con RestTemplate así:

Para la request volvemos a usar un record:

Y su test correspondiente. En este test vemos que le decimos que el webEnvironment va a ser un mock, que nos los configure automáticamente, que el doble que nos ha generado está en local y le indicamos unas coordenadas maven y un puerto para indicar que doble es el que tiene que usar.

7. Usando git como broker

Si estamos trabajando con tecnologías java, es bastante habitual tener un repositorio de artefactos maven. Esta opcion es la que nos da por defecto, pero si tener un repositorio maven es algo habitual, más habitual es tener un repositorio git, así que vamos a comentar esta alternativa aquí. Lo primero que necesitamos en un repositorio de git vacío. Cuando generamos los contratos, dentro de la carpeta target/stubs del productor tenemos la carpeta META-INF. Esa carpeta META-INF hay que subirla al repositorio. Una vez que tenemos el repositorio configurado, tenemos que decirle al productor que tiene que mandar esos contratos al repositorio.

El plugin como vemos a continuación, tenemos que configurarle para decirle:

  • Donde está el repositorio: contractsRepositoryUrl.
  • En que rama: git.branch.
  • Coordenadas del contrato en: contractDependency.
  • Modo remoto (REMOTE) en: contractsMode.
  • Goal de maven para mandarlo al repositorio: pushStubsToScm.

Si ejecutamos mvn clean install, ahora veremos que se está clonando el repositorio y esta actualizándolo:

Ahora que lo tenemos en el repositorio, necesitamos decirle a los tests del consumidor donde está el stub nuevo, así que el test nos quedaría así:

Podemos observar que las diferencias están en el modo, que ahora es remoto, la URL del repositorio y no solo el artefacto maven, y en que rama esta.

8. Conclusiones

Contract testing es una herramienta más que debemos tener en nuestra caja de herramientas, y, que si se presenta la ocasión cuando estemos desarrollando una API que van a necesitar otros equipos, deberíamos de usar para asegurarnos que cuando se cambie la versión del API no rompa nada, y que los consumidores siempre están alineados con los productores. En este tutorial hemos explorado los contratos para una API rest, pero tambien se puede usar con otras tecnologías como mensajería y grpc. Este artículo se centra en spring, pero recordemos que hay alternativas como por ejemplo pact

9. Referencias

Dejar respuesta

Please enter your comment!
Please enter your name here