¿Qué es el testing de contratos entre servicios (por ejemplo, con Pact), y cómo evita la necesidad de un entorno de integración completo?
El testing de contratos guiado por el consumidor permite que dos servicios desplegados de forma independiente demuestren que están de acuerdo en una interacción usando solo un archivo compartido, sin entorno compartido ni necesidad de tener ambos servicios corriendo.
APRENDE HACIENDOPon a prueba tu intuición3 pasos
¿Qué es el testing de contratos entre servicios (por ejemplo, con Pact), y cómo evita la necesidad de un entorno de integración completo?
Usa los ejemplos y la explicación de la lección para comprobar tu idea.
Reflexión personal. No se envía a ningún servicio de IA.#El Problema: Los Entornos de Integración No Escalan con la Cantidad de Equipos
Cuando un sistema se divide en más de un puñado de servicios desplegados de forma independiente, la estrategia de "levantamos todo y corremos tests de integración contra eso" deja de funcionar. Alguien tiene que mantener un entorno de staging compartido sincronizado con una docena de servicios desplegados por equipos distintos en calendarios distintos, poblarlo con datos de prueba compatibles, y aceptar que un solo servicio caído o a medio desplegar hace fallar los tests de todos los demás equipos por razones que no tienen nada que ver con su cambio. El entorno se convierte en un cuello de botella compartido: lento, inestable, y fuente de discusiones de "no es mi bug".
El testing de contratos resuelve un problema más acotado pero mucho más manejable: en lugar de preguntar "¿funciona bien todo el sistema junto en este momento?", pregunta "¿este proveedor sigue honrando exactamente las interacciones de las que depende este consumidor?", y responde esa pregunta sin que ninguno de los dos servicios necesite que el otro esté corriendo.
#Contratos Guiados por el Consumidor, a Grandes Rasgos
El equipo consumidor escribe un test contra un mock del proveedor, describiendo exactamente los requests que va a enviar y las respuestas que necesita recibir. Correr ese test hace dos cosas: verifica que el propio código del consumidor maneja bien la respuesta, y produce un archivo de contrato, normalmente JSON, que registra cada interacción ejercitada. Ese contrato es el artefacto de referencia. El equipo del proveedor luego corre un paso de verificación separado que reproduce cada request grabado contra su implementación real y comprueba que la respuesta real coincide con lo que el consumidor espera. Ningún lado necesita nunca que el servicio del otro esté corriendo para testear contra él: el mock hace de reemplazo durante el testing del consumidor, y el contrato grabado hace de reemplazo durante el testing del proveedor.
#Un Test de Consumidor Real
Acá tenés un test de contrato del lado consumidor para un BillingService que depende de un endpoint de OrderService, usando Pact:
// consumer.pact.test.jsconst path = require('path');const { PactV3, MatchersV3 } = require('@pact-foundation/pact');const { getOrder } = require('./orderClient');const { like, eachLike, integer } = MatchersV3;const provider = new PactV3({consumer: 'BillingService',provider: 'OrderService',dir: path.resolve(process.cwd(), 'pacts'),});describe('OrderService contract', () => {it('returns an order by id', async () => {provider.given('order 42 exists').uponReceiving('a request for order 42').withRequest({method: 'GET',path: '/orders/42',headers: { Accept: 'application/json' },}).willRespondWith({status: 200,headers: { 'Content-Type': 'application/json' },body: {id: integer(42),status: like('SHIPPED'),items: eachLike({ sku: like('SKU-1'), qty: integer(2) }),},});await provider.executeTest(async (mockServer) => {const order = await getOrder(mockServer.url, 42);expect(order.id).toBe(42);expect(order.status).toBe('SHIPPED');});});});
Fijate en los matchers: like('SHIPPED') no obliga al proveedor a devolver siempre exactamente ese string, afirma "un string con esta forma", así que el proveedor puede devolver "DELIVERED" mañana sin romper el contrato, mientras siga siendo un string. integer(42) y eachLike(...) de forma similar verifican forma y tipo en lugar de valores exactos. Este es el detalle que evita que los contratos se vuelvan tan frágiles como un test de snapshot.
#El Lado del Proveedor Verifica Contra su Implementación Real
// provider.pact.test.jsconst path = require('path');const { Verifier } = require('@pact-foundation/pact');const { startServer } = require('./server');const { seedOrder } = require('./testSupport');describe('OrderService - Pact provider verification', () => {let server;beforeAll(async () => {server = await startServer(8080);});afterAll(async () => {await server.close();});it('satisfies the BillingService contract', () => {return new Verifier({provider: 'OrderService',providerBaseUrl: 'http://localhost:8080',pactUrls: [path.resolve(process.cwd(), 'pacts/billingservice-orderservice.json')],stateHandlers: {'order 42 exists': async () => {await seedOrder({ id: 42, status: 'SHIPPED', items: [{ sku: 'SKU-1', qty: 2 }] });},},}).verifyProvider();});});
El bloque stateHandlers es lo que hace que esto funcione contra un servidor real en lugar de un mock: antes de reproducir la interacción "a request for order 42", Pact llama al handler registrado para el estado 'order 42 exists', que inserta una orden real en la base de datos real del proveedor. El proveedor está genuinamente corriendo, genuinamente manejando el request HTTP, y genuinamente devolviendo JSON real: lo único que no es "real" es que el consumidor no está presente.
#Por Qué Esto Evita un Entorno de Integración Completo
Ninguno de los dos tests necesitó que el otro servicio estuviera desplegado en ningún lado. El test del consumidor corrió contra un servidor mock en el mismo proceso que Pact levanta y apaga automáticamente. El test del proveedor corrió contra una instancia del proveedor iniciada localmente, hablando con una base de datos local o en memoria. Nada cruzó una frontera de red entre dos servicios desplegados de forma independiente, y sin embargo ambos lados tienen ahora evidencia sólida de que están de acuerdo sobre la forma de su interacción.
#Cómo se Compara con el Testing de Integración Completo
| Test de contrato | Test de integración completo | Test end-to-end | |
|---|---|---|---|
| ¿Necesita que el otro servicio esté desplegado? | No — mock (lado consumidor) o reproducción del contrato (lado proveedor) | Generalmente sí, o un doble local muy cercano | Sí, el sistema desplegado completo |
| Velocidad | Segundos | De segundos a minutos | Minutos o más |
| Qué detecta | Desajustes de interfaz/esquema entre dos servicios específicos | Comportamiento real de una dependencia real (una query real a la BD funcionando) | Si un flujo completo de usuario funciona de punta a punta |
| Qué se le escapa | Cuestiones de infraestructura: auth, políticas de red, cadenas multi-servicio, timing | Cadenas entre servicios más allá de la dependencia bajo test | La causa raíz cuando algo falla |
#Versionado y el Gate de Deploy: Dónde Entra un Broker
En la práctica, los contratos se publican en un Pact Broker (o un registro equivalente) en lugar de pasarse como archivos locales. Esto te da dos cosas que un setup solo-local no puede: un registro de qué versión de consumidor depende de qué versión de proveedor, y un chequeo can-i-deploy que un pipeline de CD puede invocar antes de liberar — "¿el contrato de cada consumidor que depende de mi API fue verificado contra esta nueva versión?" Esa pregunta, respondible en segundos contra el broker, reemplaza una porción enorme de lo que antes hacía falta un entorno de staging compartido para resolver.
#Cuándo Realmente Vale la Pena
El testing de contratos justifica su costo específicamente cuando: los servicios pertenecen a equipos distintos con ritmos de despliegue independientes, la interacción es HTTP/gRPC síncrona (o un esquema de evento, en la variante asíncrona), y la inestabilidad o lentitud del entorno de integración ya es un dolor medido. Para un monolito, o para dos servicios que siempre despliega junto el mismo equipo, el costo de coordinación que esto resuelve todavía no existe, y una suite de tests de integración normal es más simple e igual de efectiva.