Introducción a Spring Cloud Contract
En esta entrada veremos una introducción y ejemplo a Spring Cloud Contract, el cual nació para ayudarnos a implementar el Consumer Driven Contracts (CDC). Es decir, asegurar un contracto entre un Productor y un consumidor en un sistema distribuido, a través de mensajes o peticiones HTTP.
Para mostrar las principales características y el ejemplo asociado vamos a crear por un lado un consumidor y un productor, los cuales estarán asociados a través de un contrato.
En nuestro ejemplo usaremos Lombok para eliminar el boilerplate y facilitar la implementación.
Objetivo de Spring Cloud Contract
Como hemos mencionado anteriormente Spring Cloud Contract nació con el objetivo de implementar y dar validez el Consumer Driven Contracts. El cual es un patrón en el que el consumidor captura sus expectativas que le proporciona un proveedor en un contrato. Para dar validez a estos contratos y saber en qué momento se puede romper hacemos uso de Spring Cloud Contract. Entre las características principales tenemos:
- se asegura que los mensajes o las peticiones HTTP hagan exactamente lo que se ha implementado en el lado productor o servidor.
- crear pruebas de aceptación para nuestros servicios
- proporcionar una forma de publicar cambios en los contratos que son inmediatamente visibles en ambos lados de la comunicación.
- generar código de test basado en los contratos realizados en el servidor
Ejemplo Spring Cloud Contract
En este punto vamos a crear dos proyectos para hacer uso de Spring Cloud Contract. Estos proyectos harán uso de JUnit 5 para realizar la verificación de los contratos.
El ejemplo constará de un consumidor y un productor. El consumidor mediante un webclient hará una petición al productor, y estas llamadas serán las que Spring Cloud Contract verificará que no se cambian. Nuestro productor constará de dos endpoints muy simples /cars y /cars/{id}.
El ejemplo completo se encuentra en nuestro github.
Productor
La primera pieza que vamos a crear es nuestro productor, el cual tendrá dos endpoints básicos que recibirá peticiones HTTP. El productor tendrá una base de datos en memoria H2 donde se guardarán coches. La estructura será la clásica de controlador-servicio-repositorio, a continuación mostraremos las 3 capas.
Dependencias Maven
Las dedependencias que necesitamos para nuestro productor necesitaremos las siguientes dependencias:
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-contract-verifier</artifactId> <scope>test</scope> </dependency>
y también deberemos configurar nuestro spring-cloud-contract-maven-plugin.
<plugin> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-contract-maven-plugin</artifactId> <version>2.1.1.RELEASE</version> <extensions>true</extensions> <configuration> <baseClassForTests> com.refactorizando.sample.springcloudcontract.producer.BaseTestClass </baseClassForTests> </configuration> </plugin>
Creación contralador para el Productor
A continuación mostramos nuestro contralador para nuestro Productor, el cual se compone de dos endpoints básicos que harán llamadas a un servicio CarService.
@RestController @RequiredArgsConstructor @RequestMapping("/cars") public class CarController { private final CarService carService; @GetMapping public ResponseEntity<List<Car>> getCars() { var car = carService.findAll(); return new ResponseEntity<>(car, HttpStatus.OK); } @GetMapping("/{id}") public ResponseEntity<Car> getCarById(@PathVariable Long id) { var car = carService.findById(id); return new ResponseEntity<>(car, HttpStatus.OK); } }
Creación del Servicio del Productor
El servicio será una capa intermedia para llamar al repositorio:
@RequiredArgsConstructor @Service public class CarService { private final CarRepository carRepository; public Car findById(Long id) { return carRepository.findById(id).orElseThrow(); } public List<Car> findAll() { return carRepository.findAll(); } }
Creación del Repositorio del Productor
Esta es la capa de comunicación con la base de datos, para ello utilizamos JPA:
public interface CarRepository extends JpaRepository<Car, Long> { Optional<Car> findByColor(String color); }
Creación de test en Productor
Una vez hemos creado nuestra aplicción, vamos a comenzar a trabajar con Spring Cloud Contract. Para ello, vamos a añadir nuestros contratos, ya sea en REST o por mensajería en src/test/resources/contracts esta ruta es por defecto y se encuentra definida en contractsDslDir. Estos ficheros podrán ser expresados en Groovy DSL o YAML.
Nuestros contratos tendrán que verificar el endpoint /cars y /cars/{id} que hemos definido en nuestro controlador, en nuestro caso los hemos creado con Groovy y realizaremos un GET, a estos endpoints en los que verificaremos que se ha hecho correctamente la llamada.
package contracts import org.springframework.cloud.contract.spec.Contract Contract.make { description 'Should return one car in a list' request { method GET() url '/cars' } response { status OK() body '''\ [ { "id": 1, "color": "yellow", "model": "Mustang", "brand": "Ford" } ] ''' headers { contentType('application/json') } } }
Una vez hemos añadido los contratos, tenemos que añadir nuestro BaseTestClass en la ruta que hemos definido en nuestro pom.xml, es decir, en el plugin spring-cloud-contract-maven-plugin:
@ExtendWith(SpringExtension.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK) @DirtiesContext @AutoConfigureMessageVerifier public class BaseTestClass { @Autowired private CarController carController; @BeforeEach public void setup() { StandaloneMockMvcBuilder standaloneMockMvcBuilder = MockMvcBuilders.standaloneSetup(carController); RestAssuredMockMvc.standaloneSetup(standaloneMockMvcBuilder); } }
Recuerda poner @AutoConfigureMessageVerifier en la clase base de tus pruebas generadas. De lo contrario, la parte de mensajería de Spring Cloud Contract Verifier no funciona.
Cuando ejecutemos:
mvn clean install
el plugin de maven configurado generará una clase de test títulada ContractVerifierTest que extenderá de nuestra clase base y lo dejará en /target/generated-test-sources/contracts. En esta clase verás la creación de los tests que se han añadido dentro de la carpeta resources. Ten en cuenta que debe estar bien definida la ruta de los contratos en nuestro pom.xml.
public class ContractVerifierTest extends BaseTestClass { @Test public void validate_shouldReturnCars() throws Exception { // given: MockMvcRequestSpecification request = given(); // when: ResponseOptions response = given().spec(request) .get("/cars"); // then: assertThat(response.statusCode()).isEqualTo(200); assertThat(response.header("Content-Type")).matches("application/json.*"); // and: DocumentContext parsedJson = JsonPath.parse(response.getBody().asString()); assertThatJson(parsedJson).array().contains("['id']").isEqualTo(1); assertThatJson(parsedJson).array().contains("['color']").isEqualTo("yellow"); assertThatJson(parsedJson).array().contains("['model']").isEqualTo("Mustang"); assertThatJson(parsedJson).array().contains("['brand']").isEqualTo("Ford"); } @Test public void validate_shouldReturnOneCar() throws Exception { // given: MockMvcRequestSpecification request = given(); // when: ResponseOptions response = given().spec(request) .get("/cars/id"); // then: assertThat(response.statusCode()).isEqualTo(200); assertThat(response.header("Content-Type")).matches("application/json.*"); // and: DocumentContext parsedJson = JsonPath.parse(response.getBody().asString()); assertThatJson(parsedJson).field("['id']").isEqualTo(1); assertThatJson(parsedJson).field("['color']").isEqualTo("yellow"); assertThatJson(parsedJson).field("['model']").isEqualTo("Mustang"); assertThatJson(parsedJson).field("['brand']").isEqualTo("Ford"); } }
Además dentro de la carpeta target se habrá generado el Wiremock stub, con nombre spring-cloud-contract-producer-0.0.1-SNAPSHOT-stubs.jar. Este jar lo podemos disponibilizar y subir a Maven Central o a Artifactory para ser verificado durante la ejecución de nuestro cliclo de CI, para la validación de nuestros servicios.
Consumidor o cliente
El lado cliente o consumidor se encargará de consumir los stubs generados del productor, por lo que cualquier cambio en el productor provocará una ruptura del contrato.
En nuestro ejemplo crearemos un controlador que a través de un webclient invocará al servicio productor que hemos creado.
Para realizar el test de interacción con el servicio externo, en este caso con el consumidor, se hará uso en el test de wiremock para así simular la llamada.
Dependencias Maven del Consumidor
Para el lado consumidor añadiremos spring-cloud-contract-stub-runner en lugar de spring-cloud-contract-verifier y spring-cloud-contract-wiremock, para poder simular la llamada al servicio externo. No hace falta añadir el stub.jar generado anteriormente para hacer uso de el en nuestro ejemplo.
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-contract-wiremock</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-contract-stub-runner</artifactId> <scope>test</scope> </dependency>
Controlador cliente
Nuestro controlador en el lado cliente tendrá dos endpoints que se encargará de invocar a través de un webclient, al servicio productor o consumidor.
@RestController public class CarController { private WebClient webClient; @PostConstruct public void setUpWebClient() { var httpClient = HttpClient.create() .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 2_000) .doOnConnected(connection -> connection.addHandlerLast(new ReadTimeoutHandler(2)) .addHandlerLast(new WriteTimeoutHandler(2))); this.webClient = WebClient.builder() .baseUrl("http://localhost:8090") .clientConnector(new ReactorClientHttpConnector(httpClient)) .build(); } @GetMapping("/cars") public ResponseEntity<List<Car>> getCars() { var cars = webClient.get().uri("/cars").accept(MediaType.APPLICATION_JSON) .retrieve().bodyToMono(Car[].class).block(); return new ResponseEntity<>(Arrays.stream(cars).collect(Collectors.toList()), HttpStatus.OK); } @GetMapping("/cars/{id}") public ResponseEntity<Car> getCarById(@PathVariable Long id) { var car = webClient.get().uri("/cars").retrieve().bodyToMono(Car.class).block(); return new ResponseEntity<>(car, HttpStatus.OK); } }
Creación de test con Spring Cloud Contract en consumidor
A continuación vamos a ver como sería la creación de test en el consumidor haciendo uso de Spring Cloud Contract. Para ello lo que necesitamos es un test que haga uso del stub.jar que ha sido generado por nuestro productor.
@ExtendWith(SpringExtension.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @AutoConfigureStubRunner(stubsMode = StubRunnerProperties.StubsMode.LOCAL, ids = "com.refactorizando.sample:spring-cloud-contract-producer:+:stubs:8080") public class CarControllerIntegrationTest { @Autowired private CarController carController; @Test void testContractToCarsProducer() { var result = carController.getCars().getBody(); assertTrue(1 == result.size()); var car = result.get(0); assertTrue(car.getBrand().equalsIgnoreCase("Ford")); assertTrue(car.getId().equals(1L)); assertTrue(car.getColor().equalsIgnoreCase("Yellow")); assertTrue(car.getModel().equalsIgnoreCase("Mustang")); } @Test void testContractToCarByIdProducer() { var result = carController.getCarById(1L).getBody(); assertTrue(1 == result.getId()); assertTrue(result.getBrand().equalsIgnoreCase("Ford")); assertTrue(result.getId().equals(1L)); assertTrue(result.getColor().equalsIgnoreCase("Yellow")); assertTrue(result.getModel().equalsIgnoreCase("Mustang")); } }
A continuación vamos a explicar lo que significa cada componente dentro de @AutoConfigureStubRunner
- com.refactorizando.sample — es el groupId del artifact definido the groupId of our artifact.
- spring-cloud-contract-producer — es el artifactId del productor del stub.jar.
- 8080 — el puerto en el que se ejecutará el stub generado.
Este consumidor hará uso de wiremock para simular la llamada al productor. Una vez finalizado la ejecución se podrá ver si los test han devuelto el resultado esperado de la ejecución del contrato.
En este caso hemos realizado la comprobación del contrato en nuestro local con el stub.jar en nuestra carpeta .m2, aunque por ejemplo en un ciclo de CI se podría guardar de manera remota para ejecutar estos test.
Conclusión
En esta entrada hemos visto una introducción y ejemplo a Spring Cloud Contract, lo cual nos ayudará a mantener nuestros contratos entre productor y consumidor. Lo cual nos ayudará bastante en una arquitectura de microservicios, para que todos los servicios involucrados trabajen de una manera cohesionada entre si.
Si quieres puedes consultar el ejemplo completo de nuestro github.
Si necesitas más información puedes escribirnos un comentario o un correo electrónico a refactorizando.web@gmail.com y te ayudaremos encantados!
1 pensamiento sobre “Introducción a Spring Cloud Contract”