Nube de primavera: Contrato

En este artículo, le presentaremos Spring Cloud Contract, que es la respuesta de Spring a los Consumer-Driven Contracts. Hoy en día, las aplicaciones son completamente ...

Visión general

En este artículo te presentamos el Contrato de nube de primavera, que es la respuesta de Spring a los 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 extremo a extremo. 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 extremo a extremo utilizando una biblioteca como Selenio
  • Escribir pruebas de integración simulando las llamadas a otros servicios.

Si adoptamos el primer enfoque, estaríamos simulando un entorno similar al de producción. Esto requerirá más infraestructura y los comentarios llegarían tarde, ya que lleva mucho tiempo ejecutarlo.

Si adoptamos el último enfoque, tendríamos comentarios más rápidos, pero dado que estamos simulando las respuestas de llamadas externas, las simulaciones no reflejarán los cambios en el productor, si los hay.

Por ejemplo, supongamos que simulamos la llamada a un servicio externo que devuelve JSON con una clave, por ejemplo, “nombre”. Nuestras pruebas pasan y todo funciona 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 producción, en lugar de los elaborados casos de prueba.

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

Dado que el código auxiliar se versiona según el servicio del productor, el servicio del consumidor puede elegir qué versión elegir para las pruebas. Esto proporciona una retroalimentación más rápida y asegura que nuestras pruebas realmente reflejen el código.

Configuración

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

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

To focus on the topic, we would be only using these service and not other services like Eureka, Puerta, etc. that are typically included in an microservice architecture.

Detalles de configuración del productor

Comencemos con la clase POJO simple - Empleado:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class Employee {

    public Integer id;

    public String fname;

    public String lname;

    public Double salary;

    public String gender;

    // Getters and setters

Luego, tenemos un EmployeeController con un solo mapeo GET:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
@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 JSON Empleado con todos los atributos de clase como claves JSON, en función del id.

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

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

Detalles de configuración del consumidor

Del lado del consumidor, definamos otro POJO - Persona:

1
2
3
4
5
6
7
8
9
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 los nombres de los atributos sean los mismos: id, fname y lname.

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@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();
    }
}

Dado que la clase ‘Persona’ de ‘spring-cloud-contract-consumer’ tiene los mismos nombres de atributo que la clase ‘Empleado’ de ‘spring-cloud-contract-producer’, Spring asignará automáticamente los campos relevantes y nos proporcionará con el resultado.

Poniendo a prueba al consumidor

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@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 del punto final /employee/1 para devolver una respuesta JSON codificada y luego continuar con nuestra afirmación.

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

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

Implementación del contrato Spring Cloud

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 stub/recibo para que el servicio del consumidor sepa lo que está pasando.

Contrato de servicio del productor

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

1
2
3
4
5
<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 de spring-cloud-starter-contract-verifier que se envía con Lenguaje de definición de contratos (DSL) escrito en Groovy o YAML.

Vamos a crear un contrato, usando Groovy en un nuevo archivo - shouldReturnEmployeeWhenEmployeeIdFound.groovy:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 una solicitud GET a la URL /employee/1, devuelva una respuesta de estado 200 y un cuerpo JSON con 5 atributos.

Cuando se crea 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 archivo pom.xml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<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 se parece a:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
@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:

1
$ mvn clean install

spring cloud contract stubs jar

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

frasco de resguardos

Dado que realizamos la instalación, también está disponible en nuestra carpeta .m2 local. Este stub ahora puede ser utilizado por nuestro spring-cloud-contract-consumer para simular las llamadas.

Contrato de servicio al consumidor

De manera similar al lado del productor, también debemos agregar un cierto tipo de contrato a nuestro servicio al consumidor. Aquí, necesitamos agregar la dependencia spring-cloud-starter-contract-stub-runner a nuestro pom.xml:

1
2
3
4
5
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
    <scope>test</scope>
</dependency>

Ahora, en lugar de hacer nuestros simulacros locales, podemos descargar los resguardos del productor:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@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 stub creado por spring-cloud-contract-producer. El .stubsMode() es para decirle a Spring dónde debería buscar la dependencia de stub. LOCAL significa en la carpeta local .m2. Otras opciones son REMOTE y CLASSPATH.

La clase ConsumerTestContract ejecutará primero el código auxiliar y, debido a que el productor lo proporciona, somos independientes de burlarnos 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 de ruptura 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 servicio de productor y consumidor. Esto se logra creando primero un stub desde el lado del productor utilizando 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 usados ​​en este artículo se puede encontrar en GitHub.