Visión general
Contenido
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 despring-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
, fname
y 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.
Te puede interesar:Grafos en Java: Algoritmo de DijkstraCuando 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 SpliteratorSin 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.