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 鈥嬧媝or 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.

    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:

    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:

    @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.

    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.

    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:

    <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.

    Etiquetas:

    Deja una respuesta

    Tu direcci贸n de correo electr贸nico no ser谩 publicada. Los campos obligatorios est谩n marcados con *