Consumer Driven Contract: testeando servicios con Pact.

0
2843

Índice

¿Qué son los Consumer Driven Contract?

Es un patrón que nos permite testear y comprobar las diferentes interacciones entre los diferentes servicios de nuestro ecosistema y sus clientes.

En estos tests existen un mínimo de dos actores, un consumidor, que puede ser el cliente, y un proveedor, que será el servicio en sí mismo. El consumidor capturará sus expectativas respecto al proveedor y lo almacenará en un fichero que llamará ‘contrato’, y es lo que usará para realizar los tests. Un ‘contrato’ sería la definición de la forma en que va a comportarse nuestro servicio en base a una solicitud. Es decir, nosotros vamos a definir que el cliente necesitará por ejemplo, los campos nombre, apellido, y edad, por tanto lo que se va a comprobar es que esto campos le siguen llegando, independientemente de que lleguen otros campos que quizás utilicen o no otros clientes, de esta forma aunque el servicio se haya actualizado con nuevos campos, sabemos que los clientes que utilicen la versión anterior siguen teniendo soporte y que no hemos ‘roto’ nada.

Ventajas de usar este patrón

Cuando queremos probar una aplicación que se comunica con otros servicios o APIs, tenemos dos opciones:

– Desplegar todos los servicios y realizar un test E2E.

– Realizar un mock de los servicios y probar la aplicación de forma individual mediante test unitarios.

Vamos a analizar las dos opciones, vamos con la primera, tests end-to-end.

Ventajas:

– Se simula el entorno de producción.

– Se testea una comunicación real con los servicios.

Desventajas:

– Nos puede llevar mucho tiempo desplegar todos los servicios cada vez que queramos testear la aplicación.

– Introduce dependencias

– Feedback muy lento

– Muy frágil

Ahora la segunda opción, tests unitarios:

Ventajas:

– Poco tiempo de ejecución

– No requiere de infraestructura

Desventajas:

– Los servicios serían mocks que quizás no tuvieran nada que ver con la realidad

– Podrías pasar todos los test, ir a producción y fallar

En general es muy útil cuando tenemos arquitecturas basadas en microservicios, algo que está muy de moda ahora mismo.

¿Qué es Pact?

Pact es un framework de código abierto que nos permite implementar consumer driven contract de una manera simple y rápida. Tiene una comunidad bastante activa y soporta múltiples lenguajes. Puedes acceder a documentación mediante este enlace.

Logo Pact

A su vez, este framework, incluye una herramienta llamada Pact Broker, que nos permitirá compartir los contratos de forma remota.

Ejemplo con Pact y Spring Boot.

Para este ejemplo vamos a crear dos aplicaciones, una será el consumidor y otra el proveedor. Consistirá en una aplicación que devolverá los datos de un usuario cuando se le haga una petición. Empezamos creando un proyecto Maven que tenga 2 módulos: Consumer y Provider. 

En el pom.xml del proyecto ‘padre’, añadiremos las dependencias necesarias:

  • spring-boot-starter-web
  • lombok
  • spring-boot-starter-test
  • pact-jvm-consumer-java8_2.12
  • pact-jvm-provider
  • pact-jvm-consumer
  • pact-jvm-provider-spring

Y como ‘parent’, spring-boot-starter-parent.

Usaremos también la librería de Lombok, esto es opcional, es simplemente para ahorrarnos código en algunas clases. Una vez tenemos esto listo, empezaremos por el módulo del consumidor.

Consumidor

Empezamos creando la aplicación que será el consumidor, dado que será la que genere el ‘contrato’, para ello creamos un nuevo proyecto Maven. Con la siguiente estructura:

  • src
    • main
      • java
        • ConsumerApplication.java
        • User.java
    • test
      • java
        • ConsumerContractTest.java

Creamos nuestra clase modelo, User.class:

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class User {
    private String name;
    private String city;
    private int age;
}

Mediante las etiquetas @Getter y @Setter, Lombok nos genera los ‘getter’ y ‘setter’.

Ahora, la clase desde donde vamos a ‘consumir’ la API, ConsumerApplication.class:

public class ConsumerApplication {

    private final RestTemplate restTemplate;

    public ConsumerApplication(@Value("${user-service.base-url}") String baseUrl) {
        this.restTemplate = new RestTemplateBuilder().rootUri(baseUrl).build();
    }

    public User getUser (String id) {
        return restTemplate.getForObject("/users/" + id, User.class);
    }
}

Ahora vamos a crear la clase de los test que van a generar los ‘contratos’, vamos a llamarla ConsumerContractTest:

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE,
properties = "user-service.base-url:http://localhost:8080",
classes = ConsumerApplication.class)
public class ConsumerContractTest {

Añadimos esta regla para configurar nuestro ‘mock server’, el valor a null sería el host, en este caso al ser ‘localhost’ no es necesario especificarlo.

    @Rule
    public PactProviderRuleMk2 provider = new PactProviderRuleMk2("Provider", null, 8080, this);
    @Autowired
    private ConsumerApplication consumerApplication;

Aquí le indicamos que inicie el ‘mock server’, con la interacción definida en el método con el nombre ‘pactUserExists’.

    @PactVerification(fragment = "pactUserExists")
    @Test
    public void userExists() {
        User user = consumerApplication.getUser("1");
        assertEquals(user.getName(), "Pepe");
    }

Por último, en este método vamos a definir las interacciones, como vemos hemos hecho un ejemplo con un usuario de nombre Pepe, que vive en la ciudad de Madrid y tiene 22 años.

  @Pact(consumer = "Consumer")
    public RequestResponsePact pactUserExists(PactDslWithProvider builder) {
        return builder.given("User 1 exists")
                .uponReceiving("A request to /users/1")
                .path("/users/1")
                .method("GET")
                .willRespondWith()
                .status(200)
                .body(LambdaDsl.newJsonBody(o -> o
                    .stringType("name", "Pepe")
                    .stringType("city", "Madrid")
                    .numberType("age", 22)
                    ).build())
                .toPact();
    }
}

Si ejecutamos la clase ConsumerContractTest, nos generará un fichero como el siguiente:

{
    "provider": {
        "name": "Provider"
    },
    "consumer": {
        "name": "Consumer"
    },
    "interactions": [
        {
            "description": "A request to /users/1",
            "request": {
                "method": "GET",
                "path": "/users/1"
            },
            "response": {
                "status": 200,
                "headers": {
                    "Content-Type": "application/json; charset=UTF-8"
                },
                "body": {
                    "age": 22,
                    "city": "Madrid",
                    "name": "Pepe"
                },
                "matchingRules": {
                    "body": {
                        "$.name": {
                            "matchers": [
                                {
                                    "match": "type"
                                }
                            ],
                            "combine": "AND"
                        },
                        "$.city": {
                            "matchers": [
                                {
                                    "match": "type"
                                }
                            ],
                            "combine": "AND"
                        },
                        "$.age": {
                            "matchers": [
                                {
                                    "match": "number"
                                }
                            ],
                            "combine": "AND"
                        }
                    },
                    "header": {
                        "Content-Type": {
                            "matchers": [
                                {
                                    "match": "regex",
                                    "regex": "application/json;\\s?charset=(utf|UTF)-8"
                                }
                            ],
                            "combine": "AND"
                        }
                    }
                }
            },
            "providerStates": [
                {
                    "name": "User 1 exists"
                }
            ]
        }
    ...

Este será nuestro ‘contrato’, con el que vamos a verificar siempre el servicio, nos lo genera en la carpeta target/pacts/<archivo>.json, ahora este fichero lo pegamos en nuestro proveedor, dentro de la carpeta src/pacts/<archivo>.json, y con esto estaría todo configurado por parte de nuestro consumidor. Ahora vamos con el proveedor.

Proveedor

Creamos un nuevo proyecto Maven, que tendrá la siguiente estructura:

  • src
    • main
      • java
        • producer
          • User.java
          • UserController.java
          • UserService.java
          • UserServiceApplication.java
    • pacts
      • <archivo>.json
    • test
      • java
        • ContractTest.java

La clase User será nuestra clase modelo al igual que en el consumidor, pero a esta le añadiremos las etiquetas @Data y @Builder de Lombok.

La clase UserController, controlará las peticiones:

@RestController
public class UserController {
    private final UserService userService;

    public UserController (UserService userService) {
        this.userService = userService;
    }

    @GetMapping("/users/{userId}")
    public User getUser(@PathVariable String userId) {
        return userService.findUser(userId);
    }
}

Ahora creamos nuestra clase UserService, que simulará devolver un objeto de tipo User:

@Service
public class UserService {
    public User findUser(String userId) {
        return User.builder()
                .name("Paco")
                .city("Madrid")
                .age(22)
                .build();
    }
}

Los valores no tienen que coincidir con el ejemplo que hemos creado anteriormente, debido a que el test de contrato solo va a comprobar que los campos que utiliza el consumidor son los que mínimamente ofrece el proveedor.

Y por último nuestra clase UserServiceApplication, que iniciará la aplicación:

@SpringBootApplication
public class UserServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(UserServiceApplication.class, args);
    }
}

Y ahora, ya solo nos queda crear nuestra clase de test, que la llamaremos ContractTest:

@RunWith(SpringRestPactRunner.class)
@Provider("Provider")
@PactFolder("src/pacts")
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = UserServiceApplication.class)
public class ContractTest {
    @TestTarget
    public final Target target = new SpringBootHttpTarget();

    @State("User 1 exists")
    public void user1Exists() {
    }
}

Con la etiqueta @PactFolder le indicamos dónde encontrar el ‘contrato’ necesario, en la etiqueta @State colocamos el mismo nombre que tenemos dentro del ‘contrato’ en providerStates, para saber qué está testeando. Ahora ejecutamos la clase test y debería darnos ok, podemos jugar y cambiar el nombre de los campos o eliminar alguno para ver como falla, si añadimos uno más no fallará, debido a que seguirá enviando los mínimos que necesita el consumidor. Os dejo el código en Github.

Conclusiones

Como vemos, implementar los test de contrato mediante el framework de Pact es bastante sencillo, y nos permite asegurarnos de que nuestro consumidor proveedor se entienden a la perfección.

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad