¿Buscas nuestro logo?
Aquí te dejamos una copia, pero si necesitas más opciones o quieres conocer más, visita nuestra área de marca.
¿Buscas nuestro logo?
Aquí te dejamos una copia, pero si necesitas más opciones o quieres conocer más, visita nuestra área de marca.
dev
Ismail Ahmedov Hace 1 día Cargando comentarios…
La arquitectura de microservicios nos da la posibilidad de escalar nuestros servicios de forma óptima y también agilizar el desarrollo de nuevas funcionalidades, pero crea otros obstáculos que nos complican la vida. Uno de ellos es el testing.
Los microservicios se comunican con otros microservicios a través de algún protocolo, el más famoso es API Rest y, para poder reducir el Time to Market, es importante paralelizar su desarrollo. Cada microservicio puede ser creado por un equipo diferente y el objetivo es garantizar que ellos se entienden bien y entreguen un producto que funcione a la perfección sin hacer muchos ajustes. Si hacemos una analogía con una línea de ensamblaje, diferentes equipos fabrican piezas de un producto en paralelo. Cada equipo sigue especificaciones precisas para asegurar que las piezas sean compatibles y puedan ensamblarse correctamente sin necesidad de ajustes adicionales.
¿Cómo podemos probar la lógica de negocio de cada uno de los microservicios y garantizar que nada ha cambiado en la comunicación entre ellos?
Lo primero que se nos viene a la cabeza es tener un entorno de desarrollo controlado para probar nuestro microservicio (SUD).
Yo estaba trabajando en un equipo que consumía diferentes servicios de un eCommerce y consumíamos muchos servicios de diferentes dominios. En este caso utilizamos mock server para emular el comportamiento de las APIs de los otros dominios para comprobar si cumplía con los criterios de aceptación. Esto nos daba confianza y lo podíamos usar también en CI/CD.
Era demasiado bueno para que fuese verdad. Hablamos de un eCommerce que usaría un servicio que devolverá el listado de los productos. Definimos el contrato, lo publicamos en el Mock Server y empezamos el desarrollo.
Mientras estábamos desarrollando, nos dimos cuenta de que el productId en la lista de productos era redundante y podía simplificarse a id. Lo mismo ocurría con productName, que podía llamarse simplemente name.
Hablamos con el equipo que estaba desarrollando el servicio y acordamos hacer los cambios:
Generamos el contrato y actualizamos los stub servers.
Unos meses después, se decidió la expansión a otros mercados donde la divisa es distinta al euro, por lo que el campo price, que era cadena de texto, se separó en amount y currency. Era evidente que esto ocurriría, pero priorizamos lanzar el producto lo antes posible, asumiendo este trade-off desde el inicio.
Igual que antes, generamos el contrato y actualizamos los stub servers. Teníamos muchos trade-off y este approach nos estaba demorando mucho. Necesitábamos algo que nos facilite esto y lo haga de una forma automatizada. Buscando una solución conocí Consumer-Driven Contract Testing.
Consumer-Driven Contract Testing es un tipo de prueba que garantiza que un proveedor cumpla con las expectativas del consumidor. Para una API HTTP (y otros protocolos síncronos), esto implica comprobar que el proveedor acepta las solicitudes esperadas y devuelve las respuestas esperadas. Para un sistema que utiliza colas de mensajes, implica comprobar que el proveedor genera el mensaje esperado.
Hay diferentes herramientas para Contract Testing como Pact, Spring Cloud Contract, etc.
En este artículo usaremos Spring Cloud Contract para crear producer y consumer con Spring Boot 3, definir un contrato entre ellos e implementar nuevas funcionalidades garantizando que cada uno cumpla su parte del contrato.
El contrato es generado por el consumer y validado contra el producer para asegurarse de que los cambios en la API no rompan su compatibilidad.
Spring Cloud Contract es un proyecto que proporciona soluciones para implementar con éxito el enfoque de Consumer-Driven Contract Testing. Actualmente, forma parte de Spring Cloud Contract Verifier, una herramienta diseñada para facilitar la verificación de contratos en aplicaciones basadas en la JVM.
Spring Cloud Contract Verifier permite desarrollar aplicaciones basadas en contratos definidos por los consumidores. Ofrece un Lenguaje de Definición de Contratos (DSL), que puede escribirse en Groovy o YAML. A partir de estas definiciones, la herramienta genera automáticamente diversos recursos para garantizar la compatibilidad entre servicios:
Spring Cloud Contract Verifier lleva TDD al nivel de arquitectura de software.
Desde el lado del producer:
Desde el lado del consumer:
Cuando estamos trabajando con Consumer-Driven Contract Testing, tenemos tareas definidas para realizar el trabajo completo.
¿Qué pasa si uno de los equipos necesita cambiar el contrato?
Levanta la mano 🙋 y pide una reunión con el otro equipo para refinar el contrato (como en el paso 1), pero se hacen modificaciones en la última versión del contrato y se sigue con los pasos 2 y 3.
Vamos a crear un proyecto multimódulo de Maven que tendría 2 módulos:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.paradigmadigital</groupId>
<artifactId>spring-boot-3-consumer-driven-contract-testing</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<modules>
<module>catalog</module>
<module>shop</module>
</modules>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>
Vamos a la página favorita de Josh Long: Spring initializr y generamos el producer incluyendo las siguientes dependencias:
Generamos el proyecto y extraemos su contenido en el módulo catalog.
En la ruta catalog/src/test/resources/ tendremos una carpeta contracts. Si no existe la creamos, porque en ella vamos a añadir los contratos. Podemos escribirlos en Java o utilizar alguno de los DSL. Spring Cloud Contract ofrece dos tipos de DSL: uno escrito en Groovy y otro en YAML. Vamos a ver cómo se escriben los contratos en cada uno de los formatos soportados.
El contrato será del ejemplo que vimos antes, si se hace una petición GET al endpoint /products se devolverá la lista de los productos definidos en el fichero products.json:
{
"products": [
{
"productId": "17035535",
"productName": "Cheap product",
"price": "9.99 EUR"
},
{
"productId": "17005954",
"productName": "Quality product",
"price": "29.99 EUR"
}
]
}
Definición del contrato en Java:
public class shouldReturnProducts implements Supplier<Collection<Contract>> {
@Override
public Collection<Contract> get() {
return Collections.singletonList(Contract.make(contract -> {
contract.description("Should return products list");
contract.name("Java contract");
contract.request(request -> {
request.url("/products");
request.method(request.GET());
});
contract.response(response -> {
response.status(response.OK());
response.headers(header -> {
header.contentType(header.applicationJson());
});
response.body(response.file("response/products.json"));
});
}));
}
}
Definición del contrato en Groovy:
import org.springframework.cloud.contract.spec.Contract
Contract.make {
description "Should return products list"
name "Groovy contract"
request {
method GET()
url '/products'
}
response {
status OK()
headers {
contentType(applicationJson())
}
body(file("response/products.json"))
}
}
Definición del contrato en YAML:
description: yaml contract
request:
method: GET
url: /products
response:
status: 200
headers:
Content-Type: application/json
bodyFromFile: response/products.json
Los 3 contratos generan el mismo código Java:
@Test
public void validate_shouldReturnProducts() throws Exception {
// given:
MockMvcRequestSpecification request = given();
// when:
ResponseOptions response = given().spec(request)
.get("/products");
// then:
assertThat(response.statusCode()).isEqualTo(200);
assertThat(response.header("Content-Type")).isEqualTo("application/json");
// and:
DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
assertThatJson(parsedJson).array("['products']").contains("['productId']").isEqualTo("17035535");
assertThatJson(parsedJson).array("['products']").contains("['productName']").isEqualTo("Cheap product");
assertThatJson(parsedJson).array("['products']").contains("['price']").isEqualTo("9.99 EUR");
assertThatJson(parsedJson).array("['products']").contains("['productId']").isEqualTo("17005954");
assertThatJson(parsedJson).array("['products']").contains("['productName']").isEqualTo("Quality product");
assertThatJson(parsedJson).array("['products']").contains("['price']").isEqualTo("29.99 EUR");
}
Vemos que tenemos tests generados automáticamente dependiendo del contrato. Es impresionante.
Nos falta configurar la clase principal para que los tests de contrato se puedan ejecutar correctamente. En nuestro caso tendríamos:
@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@Sql({"/sql/schema_test.sql", "/sql/test_products.sql"})
public class BaseTestClass {
@Autowired
private ProductController productController;
@BeforeEach
public void setup() {
StandaloneMockMvcBuilder standaloneMockMvcBuilder = MockMvcBuilders.standaloneSetup(productController);
RestAssuredMockMvc.standaloneSetup(standaloneMockMvcBuilder);
}
}
Una vez creado el BaseTestClass.java, debemos configurarlo para que pueda funcionar con el plugin de Maven para Spring Cloud Contract.
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<version>4.2.1</version>
<extensions>true</extensions>
<configuration>
<testFramework>JUNIT5</testFramework>
<baseClassForTests>com.paradigmadigital.catalog.BaseTestClass</baseClassForTests>
</configuration>
</plugin>
Con esto ya podemos generar los stubs sin que hayamos implementado la lógica de negocio con el siguiente comando:
mvn clean install -pl catalog
Lo ideal sería que los stubs se publiquen en el Artifactory como parte del pipeline de CI/CD. De esta manera, los dos equipos pueden trabajar en paralelo.
Volvemos de nuevo a Spring initializr generamos el consumer incluyendo los siguientes dependencias:
Generamos el proyecto y extraemos su contenido en el módulo shop.
Los tests de integración del consumidor utilizan los stubs generados por el equipo del productor. Esta es la manera simple de un tests de integración:
@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
@AutoConfigureStubRunner(stubsMode = StubRunnerProperties.StubsMode.LOCAL,
ids = "com.paradigmadigital:catalog:+:stubs:8080")
class ShopControllerIntegrationTest {
@Autowired
private ShopController shopController;
@Test
void testContractToCatalogProducer() {
var result = shopController.getProducts().getBody();
assertNotNull(result);
assertThat(result.products()).extracting("id")
.contains(17035535L, 17005954L);
}
}
Con la anotación AutoConfigureStubRunner configuramos:
El equipo encargado del consumidor (Shop Team) casi ha terminado la implementación, pero se ha detectado que se podía mejorar el contrato. Se ha eliminado la redundancia:
Han acordado estos cambios con el equipo que está encargado de producer (Catalog Team), han modificado el contrato y han publicado nueva versión de los stubs. Y los dos equipos siguen el desarrollo.
El proceso de trabajo es idéntico cuando el campo price, que era cadena de texto, se separó en amount y currency.
Hemos visto cómo Spring Cloud Contract facilita la gestión de contratos entre un consumidor y un proveedor de servicios, permitiendo implementar nuevos cambios sin temor a romper la integración.
Gracias a Spring Cloud Contract Verifier y Stub Runner, obtenemos una retroalimentación rápida sin necesidad de desplegar todo el ecosistema de microservicios. Esto garantiza que los stubs utilizados en el desarrollo del cliente reflejan con precisión el comportamiento real del servidor.
Podéis ver todo el código con los commits por pasos en el repositorio GitHub: Example of Consumer Driven Contract Testing with Spring Boot 3.
Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.
Cuéntanos qué te parece.