Imagina que tienes dos microservicios: el servicio "Consumidor" y el servicio "Producto". El primero quiere obtener información sobre productos del segundo. Parece sencillo, ¿pero qué pasa si el equipo que mantiene el servicio "Producto" decide cambiar la estructura de los objetos JSON que devuelve? Sí, el servicio "Consumidor" empezará a fallar alegremente. Para que eso no pase usamos contratos, que nos permiten asegurarnos de que la interacción entre servicios sigue "en la misma onda".
¿Qué es Pact?
Pact — es un framework para pruebas contractuales que ayuda a garantizar que dos microservicios (o cliente/servidor) interactúan tal como se prometieron. Pact trabaja con el patrón "consumer-provider":
- Consumidor (Consumer) crea un contrato que describe cómo espera que el provider se comporte.
- Proveedor (Provider) verifica que su funcionalidad cumple con el contrato y actualiza el contrato si hace cambios.
Ejemplo de esquema simple de Pact:
[Servicio consumidor -> Genera el contrato -> El contrato se guarda -> Proveedor verifica el contrato]
Tarea: Implementemos pruebas contractuales
Para nuestro ejemplo tendremos:
- Consumidor — el microservicio
CustomerService, que llama al API del servicioProductService. - Proveedor — el microservicio
ProductService, que provee datos sobre los productos.
Objetivo: Escribir tests para CustomerService y comprobar que ProductService cumple el contrato.
Paso 1: Preparar el entorno
Para trabajar con Pact en Gradle añadiremos las siguientes dependencias:
dependencies {
testImplementation 'au.com.dius.pact.provider:junit5:4.6.2'
testImplementation 'au.com.dius.pact.consumer:junit5:4.6.2'
}
Ahora tenemos todo para trabajar con Pact tanto en el lado del consumidor como en el del proveedor.
Paso 2: Escribamos el contrato para el consumidor
Crearemos la clase de test CustomerServicePactTest, donde comprobaremos la interacción con ProductService.
import au.com.dius.pact.consumer.Pact;
import au.com.dius.pact.consumer.dsl.PactDslWithProvider;
import au.com.dius.pact.consumer.junit5.PactConsumerTestExt;
import au.com.dius.pact.consumer.junit5.PactTestFor;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
@ExtendWith(PactConsumerTestExt.class)
public class CustomerServicePactTest {
@Pact(consumer = "CustomerService", provider = "ProductService")
public Map<String, Object> createPact(PactDslWithProvider builder) {
return builder
.given("Existe un producto con ID 1")
.uponReceiving("petición para obtener información del producto")
.path("/products/1")
.method("GET")
.willRespondWith()
.status(200)
.body("{\"id\": 1, \"name\": \"Laptop\", \"price\": 999.99}")
.toPact();
}
@Test
@PactTestFor(providerName = "ProductService", port = "8080")
void testGetProduct() {
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> response = restTemplate.getForEntity("http://localhost:8080/products/1", String.class);
assertEquals(200, response.getStatusCodeValue());
assertEquals("{\"id\": 1, \"name\": \"Laptop\", \"price\": 999.99}", response.getBody());
}
}
Qué pasa aquí:
- En el método
createPactdescribimos el contrato:- En la condición (given), que existe un producto con ID 1
- Si el cliente solicita
/products/1, devuelve un objeto JSON con los datos del producto
- En el test
testGetProductcomprobamos queCustomerServiceenvía correctamente la petición y recibe la respuesta esperada.
Este test genera un contrato, que se guarda como un archivo JSON.
Ejemplo de contrato:
{
"provider": { "name": "ProductService" },
"consumer": { "name": "CustomerService" },
"interactions": [
{
"description": "petición para obtener información del producto",
"request": {
"method": "GET",
"path": "/products/1"
},
"response": {
"status": 200,
"body": { "id": 1, "name": "Laptop", "price": 999.99 }
}
}
]
}
Paso 3: Verificamos el contrato en el lado del proveedor
Ahora vamos a ProductService. Aquí comprobamos que el API de este servicio cumple el contrato.
Crearemos la clase de test ProductServicePactTest:
import au.com.dius.pact.provider.junit5.PactVerificationContext;
import au.com.dius.pact.provider.junit5.PactVerificationInvocationContextProvider;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@ExtendWith(PactVerificationInvocationContextProvider.class)
public class ProductServicePactTest {
@BeforeEach
void before(PactVerificationContext context) {
context.setTarget(new HttpTestTarget("localhost", 8080));
}
@Test
void pactVerificationTest(PactVerificationContext context) {
context.verifyInteraction();
}
}
Lo esencial aquí:
HttpTestTargetindica dónde está el API de nuestro proveedor (localhost:8080).- El método
verifyInteractioncarga el contrato que generamos para el consumidor y verifica queProductServicecumple con las interacciones descritas.
Paso 4: Ejecutemos todo junto
- Levanta
ProductServiceenlocalhost:8080. Asegúrate de que devuelve los datos correctos para la petición/products/1. - Primero ejecuta el test
CustomerServicePactTestpara generar el contrato. - Luego ejecuta
ProductServicePactTestpara verificar el proveedor.
Manejo de errores y problemas típicos
- Modificación del API del proveedor: si el equipo de
ProductServicemodifica el API, los tests del proveedor empezarán a fallar. Eso es una señal — toca actualizar el contrato y coordinar los cambios con el consumidor. - Desajuste de datos en el contrato: si el proveedor devuelve campos adicionales, no siempre es un error, pero hay que hablarlo con el equipo del consumidor.
- Múltiples contratos: si
ProductServiceatiende a varios consumidores, los contratos de cada uno deben verificarse por separado.
¿Para qué sirve todo esto en la práctica?
Las pruebas contractuales ayudan a detectar problemas de integración entre microservicios antes de que salgan a producción. Esto es especialmente importante en equipos que trabajan en servicios distintos, cuando cambios en un lado pueden "romper" al otro sin que nadie se dé cuenta.
Los contratos también encajan muy bien en sistemas de CI/CD: se pueden ejecutar los tests como parte del pipeline de CI/CD para automatizar la verificación de cambios.
¡Felicidades! Ahora sabes escribir pruebas contractuales con Pact. Es un paso importante hacia sistemas de microservicios más fiables y de calidad. ¡Que vivan las integraciones estables!
GO TO FULL VERSION