Spring Cloud: Contrato

S

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.

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.

About the author

Ramiro de la Vega

Bienvenido a Pharos.sh

Soy Ramiro de la Vega, Estadounidense con raíces Españolas. Empecé a programar hace casi 20 años cuando era muy jovencito.

Espero que en mi web encuentres la inspiración y ayuda que necesitas para adentrarte en el fantástico mundo de la programación y conseguir tus objetivos por difíciles que sean.

Add comment

Sobre mi

Últimos Post

Etiquetas

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con tus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, aceptas el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Más información
Privacidad