Spring Cloud: Contrato

    Visión general

    En este artículo, te presentaremos Contrato de Spring Cloud, que es la respuesta de Spring a Contratos impulsados ​​por el consumidor.

    Hoy en día, las aplicaciones se prueban exhaustivamente, ya sean pruebas unitarias, pruebas de integración o pruebas de un extremo a otro. Es muy común en una arquitectura de microservicio que un servicio (consumidor) se comunique con otro servicio (productor) para completar una solicitud.

    Para probarlos, tenemos dos opciones:

    • Implemente todos los microservicios y realice pruebas de un extremo a otro utilizando una biblioteca como Selenio
    • Escribe pruebas de integración burlándote de las llamadas a otros servicios

    Si adoptamos el primer enfoque, estaríamos simulando un entorno similar a la producción. Esto requerirá más infraestructura y la retroalimentación llegaría tarde, ya que lleva mucho tiempo ejecutarla.

    Si adoptamos el último enfoque, obtendríamos una retroalimentación más rápida, pero como nos burlamos de las respuestas de llamadas externas, las simulaciones no reflejarán cambios en el productor, si los hay.

    Por ejemplo, supongamos que nos burlamos de la llamada a un servicio externo que devuelve JSON con una clave, digamos, name. Nuestras pruebas pasan y todo está funcionando bien. A medida que pasa el tiempo, el otro servicio ha cambiado la clave a fname.

    Te puede interesar:Patrones de diseño en Java

    Nuestros casos de prueba de integración seguirán funcionando bien. Es probable que el problema se note en un entorno de ensayo o de producción, en lugar de en los casos de prueba elaborados.

    Spring Cloud Contract nos proporciona el Spring Cloud Contract Verifier exactamente para estos casos. Crea un código auxiliar del servicio de productor que puede ser utilizado por el servicio de consumidor para simular las llamadas.

    Dado que el stub se versiona de acuerdo con el servicio del productor, el servicio al consumidor puede elegir qué versión elegir para las pruebas. Esto proporciona comentarios más rápidos y asegura que nuestras pruebas realmente reflejen el código.

    Preparar

    Para demostrar el concepto de contratos, contamos con los siguientes servicios back-end:

    • Spring-nube-contrato-productor: Un servicio REST simple que tiene un único punto final de /employee/{id}, que produce una respuesta JSON.
    • consumidor-contrato-nube-Spring: Un cliente consumidor simple que llama /employee/{id} punto final de spring-cloud-contract-producer para completar su respuesta.

    Para centrarnos en el tema, solo estaríamos usando estos servicios y no otros servicios como Eureka, Gateway, etc. que normalmente se incluyen en una arquitectura de microservicio.

    Detalles de la configuración del productor

    Comencemos con la clase POJO simple: Employee:

    Te puede interesar:Patrones de diseño Java J2EE
    public class Employee {
    
        public Integer id;
    
        public String fname;
    
        public String lname;
    
        public Double salary;
    
        public String gender;
    
        // Getters and setters
    

    Entonces, tenemos un EmployeeController con un solo GET cartografía:

    @RestController
    public class EmployeeController {
    
        @Autowired
        EmployeeService employeeService;
    
        @GetMapping(value = "employee/{id}")
        public ResponseEntity<?> getEmployee(@PathVariable("id") int id) {
            Optional<Employee> employee = employeeService.findById(id);
            if (employee.isPresent()) {
                return ResponseEntity.status(HttpStatus.OK).contentType(MediaType.APPLICATION_JSON).body(employee.get());
            } else {
                return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
            }
        }
    }
    

    Es un controlador simple que devuelve un Employee JSON con todos los atributos de clase como claves JSON, basadas en el id.

    EmployeeService podría ser cualquier cosa que encuentre al empleado por id, en nuestro caso, es una implementación simple de JpaRepository:

    public interface EmployeeService extends JpaRepository<Employee, Integer> {}
    

    Detalles de configuración del consumidor

    Por el lado del consumidor, definamos otro POJO: Person:

    class Person {
    
        private int id;
    
        public String fname;
    
        public String lname;
    
        // Getters and setters
    

    Tenga en cuenta que el nombre de la clase no importa, siempre que el nombre de los atributos sea el mismo: id, fnamey lname.

    Ahora, suponga que tenemos un componente que llama al /employee/{id} punto final de spring-cloud-contract-producer:

    Te puede interesar:Patrones de diseño estructural en Java
    @Component
    class ConsumerClient {
    
        public Person getPerson(final int id) {
            final RestTemplate restTemplate = new RestTemplate();
    
            final ResponseEntity<Person> result = restTemplate.exchange("http://localhost:8081/employee/" + id,
                    HttpMethod.GET, null, Person.class);
    
            return result.getBody();
        }
    }
    

    Desde el Person clase de spring-cloud-contract-consumer tiene los mismos nombres de atributo que el del Employee clase de spring-cloud-contract-producer – Spring mapeará automáticamente los campos relevantes y nos proporcionará el resultado.

    Probando al consumidor

    Ahora, si quisiéramos probar el servicio al consumidor, haríamos una prueba simulada:

    @SpringBootTest(classes = SpringCloudContractConsumerApplication.class)
    @RunWith(SpringRunner.class)
    @AutoConfigureWireMock(port = 8081)
    @AutoConfigureJson
    public class ConsumerTestUnit {
    
        @Autowired
        ConsumerClient consumerClient;
    
        @Autowired
        ObjectMapper objectMapper;
    
        @Test
        public void clientShouldRetrunPersonForGivenID() throws Exception {
            WireMock.stubFor(WireMock.get(WireMock.urlEqualTo("/employee/1")).willReturn(
                    WireMock.aResponse()
                            .withStatus(200)
                            .withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_UTF8_VALUE)
                            .withBody(jsonForPerson(new Person(1, "Jane", "Doe")))));
            BDDAssertions.then(this.consumerClient.getPerson(1).getFname()).isEqualTo("Jane");
        }
    
        private String jsonForPerson(final Person person) throws Exception {
            return objectMapper.writeValueAsString(person);
        }
    }
    

    Aquí, nos burlamos del resultado de la /employee/1 endpoint para devolver una respuesta JSON codificada y luego continuar con nuestra aserción.

    Ahora bien, ¿qué pasa si cambiamos algo en el productor?

    El código que prueba al consumidor no reflejará ese cambio.

    Implementación de Spring Cloud Contract

    Para asegurarnos de que estos servicios estén «en la misma página» cuando se trata de cambios, les proporcionamos a ambos un contrato, tal como lo haríamos con los humanos.

    Te puede interesar:Grafos en Java: Algoritmo de Dijkstra

    Cuando se cambia el servicio del productor, se crea un talón / recibo para el servicio al consumidor para informarle lo que está sucediendo.

    Contrato de servicio al productor

    Para implementar esto, primero, agreguemos el spring-cloud-starter-contract-verifier dependencia de nuestro productor pom.xml:

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-contract-verifier</artifactId>
        <scope>test</scope>
    </dependency>
    

    Ahora, necesitamos definir un contrato basado en el cual Spring Cloud Contract ejecutará pruebas y construirá un stub. Esto se hace a través del spring-cloud-starter-contract-verifier que se envía con el lenguaje de definición de contratos (DSL) escrito en Groovy o YAML.

    Creemos un contrato, usando Groovy en un nuevo archivo – shouldReturnEmployeeWhenEmployeeIdFound.groovy:

    import org.springframework.cloud.contract.spec.Contract
    
    Contract.make {
      description("When a GET request with an Employee id=1 is made, the Employee object is returned")
      request {
        method 'GET'
        url '/employee/1'
      }
     response {
        status 200
    body("""
      {
        "id": "1",
        "fname": "Jane",
        "lname": "Doe",
        "salary": "123000.00",
        "gender": "M"
      }
      """)
        headers {
          contentType(applicationJson())
        }
      }
    }
    

    Este es un contrato bastante simple que define un par de cosas. Si hay un GET solicitud a la URL /employee/1, devuelve una respuesta de estado 200 y un cuerpo JSON con 5 atributos.

    Cuando se compila la aplicación, durante la fase de prueba, Spring Cloud Contract creará clases de prueba automáticas que leerán este archivo Groovy.

    Te puede interesar:Interfaz iterable de Java: Iterator, ListIterator y Spliterator

    Sin embargo, para hacer posible que las clases de prueba se generen automáticamente, necesitamos crear una clase base que puedan extender. Para registrarlo como la clase base para las pruebas, lo agregamos a nuestro pom.xml archivo:

    <plugin>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-contract-maven-plugin</artifactId>
        <extensions>true</extensions>
        <configuration>
            <baseClassForTests>
                com.mynotes.springcloud.contract.producer.BaseClass
            </baseClassForTests>
        </configuration>
    </plugin>
    

    Nuestra BaseClass parece algo como:

    @SpringBootTest(classes = SpringCloudContractProducerApplication.class)
    @RunWith(SpringRunner.class)
    public class BaseClass {
    
        @Autowired
        EmployeeController employeeController;
    
        @MockBean
        private EmployeeService employeeService;
    
        @Before
        public void before() {
            final Employee employee = new Employee(1, "Jane", "Doe", 123000.00, "M");
            Mockito.when(this.employeeService.findById(1)).thenReturn(Optional.of(employee));
            RestAssuredMockMvc.standaloneSetup(this.EmployeeController);
        }
    }
    

    Ahora, construyamos nuestra aplicación:

    $ mvn clean install
    

    Nuestra target carpeta, además de las compilaciones normales, ahora contiene una stubs jar también:

    Desde que realizamos install, también está disponible en nuestro local .m2 carpeta. Este stub ahora puede ser utilizado por nuestro spring-cloud-contract-consumer burlarse de las llamadas.

    Contrato de servicio al consumidor

    Al igual que en el lado del productor, también necesitamos agregar un cierto tipo de contrato a nuestro servicio al consumidor. Aquí, necesitamos agregar spring-cloud-starter-contract-stub-runner dependencia a nuestra pom.xml:

    Te puede interesar:Métodos de objetos de Java: hashCode ()
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
        <scope>test</scope>
    </dependency>
    

    Ahora, en lugar de hacer nuestras simulaciones locales, podemos descargar los stubs del productor:

    @SpringBootTest(classes = SpringCloudContractConsumerApplication.class)
    @RunWith(SpringRunner.class)
    public class ConsumerTestContract {
    
        @Rule
        public StubRunnerRule stubRunnerRule = new StubRunnerRule()
            .downloadStub("com.mynotes.spring-cloud", "spring-cloud-contract-producer", "0.0.1-SNAPSHOT", "stubs")
            .withPort(8081)
            .stubsMode(StubRunnerProperties.StubsMode.LOCAL);
    
        @Autowired
        ConsumerClient consumerClient;
    
        @Test
        public void clientShouldRetrunPersonForGivenID_checkFirsttName() throws Exception {
            BDDAssertions.then(this.consumerClient.getPerson(1).getFname()).isEqualTo("Jane");
        }
    
        @Test
        public void clientShouldRetrunPersonForGivenID_checkLastName() throws Exception {
            BDDAssertions.then(this.consumerClient.getPerson(1).getLname()).isEqualTo("Doe");
        }
    }
    

    Como puede ver, usamos el código auxiliar creado por spring-cloud-contract-producer. los .stubsMode() es decirle a Spring dónde debería buscar la dependencia de stub. LOCAL significa en el local .m2 carpeta. Otras opciones son REMOTE y CLASSPATH.

    los ConsumerTestContract class ejecutará el stub primero y debido a su proveedor por parte del productor, somos independientes de burlarse de la llamada externa. Si supongamos que el productor cambió el contrato, se puede averiguar rápidamente a partir de qué versión se introdujo el cambio fundamental y se pueden tomar las medidas adecuadas.

    Conclusión

    Hemos cubierto cómo usar Spring Cloud Contract para ayudarnos a mantener un contrato entre un productor y un servicio al consumidor. Esto se logra creando primero un stub desde el lado del productor usando un Groovy DSL. Este código auxiliar generado se puede utilizar en el servicio al consumidor para simular llamadas externas.

    Como siempre, el código de los ejemplos utilizados en este artículo se puede encontrar en GitHub.

    Rate this post

    Etiquetas: