Módulo 3: Contract testing: el contrato consumer/provider

7. El concepto de Pact

Descripción

Todo lo que construiste en este módulo —el contrato como spec de comportamiento, dirigido por el consumer, verificado con una batería parametrizada contra varias implementaciones— existe también, industrializado, para un caso que Reservo aún no ha tocado: dos componentes que no viven en el mismo proceso de Python, sino que son servicios separados que se hablan por la red. Cuando BookingService y PaymentGateway dejan de ser dos objetos en la misma memoria y pasan a ser dos aplicaciones distintas —quizás en máquinas distintas, escritas por equipos distintos, desplegadas por separado—, la costura entre ellos ya no es una llamada a un método: es una petición HTTP. Y la divergencia del módulo 2 se vuelve más peligrosa, porque ahora el provider puede cambiar y desplegarse sin que el consumer se entere hasta que algo se rompe en producción.

La herramienta estándar de la industria para este problema se llama Pact, y esta lección te enseña su concepto —no su instalación—. Pact es la versión en red y automatizada de lo que hiciste a mano: contratos consumer-driven entre servicios. La idea central es la misma que ya dominas —el consumer define qué espera, el provider promete cumplirlo—, pero con dos piezas nuevas que la red obliga a añadir: un pact file (el contrato serializado a un archivo JSON, para poder cruzar la frontera entre dos bases de código) y un broker (un lugar central donde el consumer publica su pact file y el provider lo recoge para verificarse). Vas a entender esas piezas, ver un pact file de verdad, y mapear cada una a algo que ya construiste en este módulo.

Conexión con el módulo: esta lección es el puente entre lo que hiciste a mano y lo que la industria hace a escala. No cambia lo que aprendiste; lo sitúa. La batería parametrizada de las lecciones 4 y 5 es un contrato consumer-driven en proceso; Pact es el mismo patrón cuando la costura es HTTP y los dos lados son servicios independientes. Al terminar sabrás qué problema resuelve Pact, cuándo vale la pena, y por qué esta guía te enseña la idea sin instalar la herramienta —Reservo se mantiene en la stdlib pura, y el patrón, no el paquete, es lo que te llevas—.

Analogía: el contrato notariado entre dos empresas

Cuando dos personas que se conocen hacen un trato, un apretón de manos basta: están en la misma habitación, se hablan directo, y si algo falla lo resuelven ahí mismo. Así es tu contrato hecho a mano: BookingService y el repositorio viven en el mismo proceso, "se dan la mano" con una llamada a un método, y la batería verifica el trato en el acto. Pero cuando dos empresas distintas hacen un trato —proveedor y cliente, en ciudades distintas, que quizás nunca se ven—, el apretón de manos no alcanza. Escriben el acuerdo en un documento, lo llevan a un notario que guarda una copia oficial, y cada parte puede consultar esa copia para verificar que cumple. El documento cruza la distancia que las manos no pueden; el notario es el lugar neutral donde ambas partes confían.

Pact es ese documento notariado para servicios. El pact file es el documento escrito del acuerdo —qué peticiones mandará el consumer y qué respuestas espera—, capaz de viajar entre dos bases de código que no comparten memoria. El broker es el notario: el lugar central donde el consumer deposita el pact file y donde el provider va a recogerlo para comprobar que lo cumple. La distancia que obliga a escribir y notariar el trato es la red: en proceso, un método basta; entre servicios, hace falta un artefacto que cruce la frontera y un lugar neutral que lo custodie. Esa es toda la diferencia entre tu batería a mano y Pact —el trato es el mismo; cambia cómo se registra y se comparte cuando las partes están lejos—.

Las tres piezas de Pact

Pact organiza el contrato consumer-driven entre servicios en tres piezas. Reconócelas por su equivalente en lo que ya hiciste:

  1. El test del consumer que genera el pact file. En Pact, el consumer escribe un test contra un provider simulado (un mock server que Pact levanta). En ese test declara: "voy a mandar esta petición, y espero esta respuesta". Al correr el test, Pact graba esas expectativas y las escribe en un archivo JSON: el pact file. Es tu contrato, pero serializado a un artefacto en vez de vivir como funciones de pytest. El equivalente en tu módulo: las cláusulas que definiste desde las necesidades del consumer (lección 3).

  2. El broker que comparte el pact file. El consumer publica su pact file en el Pact Broker: un servicio central que almacena los contratos, los versiona, sabe qué versión del consumer produjo cuál, y puede avisar al provider cuando hay uno nuevo. Es la pieza que no existe en tu versión a mano porque no la necesitas: tus dos "servicios" son el mismo proceso, así que el contrato no tiene que viajar a ningún lado. En cuanto los servicios se separan, alguien tiene que custodiar y repartir el contrato —ese es el broker—.

  3. La verificación del provider. El provider recoge el pact file (del broker) y corre la verificación: por cada interacción declarada, reproduce la petición contra el provider real y comprueba que la respuesta coincide con la que el consumer esperaba. Si coincide, el provider cumple el contrato; si no, la verificación falla —igual que tu [sqlite] o [buggy-fake] en la batería—. El equivalente en tu módulo: correr la batería contra una implementación (la lección 4). La verificación del provider es eso, con la petición viajando por HTTP en vez de ser una llamada a un método.

Fíjate en el mapeo, porque es la clave de la lección: consumer que define expectativas (tu lección 3) → test del consumer que genera el pact file; contrato compartido (tu batería) → pact file + broker; verificar cada implementación (tu lección 4) → verificación del provider. Pact no inventa un concepto nuevo; toma el que ya tienes y le añade lo que la red exige: serializar el contrato y tener un lugar para compartirlo.

Ejemplo: un pact file de verdad

Un pact file es un JSON. No hace falta la herramienta para entender su forma —es legible—. Este es el contrato del PaymentGateway visto por su consumer BookingService, escrito en el formato de Pact:

{
  "consumer": { "name": "BookingService" },
  "provider": { "name": "PaymentGateway" },
  "interactions": [
    {
      "description": "un cobro de una reserva pro de 3 horas",
      "providerStates": [ { "name": "el socio tiene fondos suficientes" } ],
      "request": {
        "method": "POST",
        "path": "/charges",
        "body": { "amount_cents": 6000 }
      },
      "response": {
        "status": 200,
        "body": { "ok": true, "amount_cents": 6000 }
      }
    }
  ],
  "metadata": {
    "pactSpecification": { "version": "3.0.0" }
  }
}

Léelo con los ojos del módulo. consumer y provider nombran los dos lados de la costura —los mismos roles de la lección 3—. interactions es la lista de cláusulas, cada una con una request (lo que el consumer manda: POST /charges con amount_cents: 6000) y una response (lo que espera de vuelta: 200 con ok: true). Reconoces aquí los dos estilos de la lección 6: la request/response es una cláusula de interacción (afirma sobre la conversación), y el providerStates —"el socio tiene fondos suficientes"— es cómo Pact prepara el estado del provider antes de reproducir la petición, para que la respuesta sea la esperada. metadata fija la versión del formato. Es tu contrato, exactamente, en forma de archivo.

Ejemplo trabajado: el flujo, ilustrado a mano

Para que el flujo no quede abstracto, veámoslo concreto con una ilustración hecha a mano con la stdlibjson, nada de Pact instalado—. No es Pact; es una maqueta de su idea, para que veas las dos mitades (consumer genera, provider verifica) funcionando de verdad. Primero, el paso del consumer, que escribe el pact file:

# pact_demo/generate_pact.py (extracto) — el CONSUMER declara y escribe el pact
pact = {
    "consumer": {"name": "BookingService"},
    "provider": {"name": "PaymentGateway"},
    "interactions": [
        {
            "description": "un cobro de una reserva pro de 3 horas",
            "providerStates": [{"name": "el socio tiene fondos suficientes"}],
            "request": {"method": "POST", "path": "/charges",
                        "body": {"amount_cents": 6000}},
            "response": {"status": 200,
                         "body": {"ok": True, "amount_cents": 6000}},
        }
    ],
    "metadata": {"pactSpecification": {"version": "3.0.0"}},
}
Path("BookingService-PaymentGateway.json").write_text(json.dumps(pact, indent=2))

Luego, el paso del provider, que carga el pact file y verifica cada interacción contra el provider real:

# pact_demo/verify_pact.py (extracto) — el PROVIDER se verifica contra el pact
class RealPaymentGateway:
    def charge(self, amount_cents):
        return {"ok": True, "amount_cents": amount_cents}


def verify(pact_path, provider):
    pact = json.loads(Path(pact_path).read_text())
    for i in pact["interactions"]:
        expected = i["response"]["body"]
        actual = provider.charge(i["request"]["body"]["amount_cents"])
        ok = actual == expected
        print(f"  [{'OK ' if ok else 'FALLA'}] {i['description']}")

Qué esperar. En mi máquina (Python 3.14.0, stdlib):

python3 pact_demo/generate_pact.py
python3 pact_demo/verify_pact.py
pact file escrito: BookingService-PaymentGateway.json
interacciones declaradas por el consumer: 1
verificando provider 'PaymentGateway' contra el pact de 'BookingService'
  [OK ] un cobro de una reserva pro de 3 horas
verificacion del provider terminada

Ahí está el flujo entero de Pact, en miniatura: el consumer declaró su expectativa y la escribió en un pact file; el provider lo cargó y verificó que cumple cada interacción. Es exactamente la forma de tu batería parametrizada —definir el contrato, correrlo contra una implementación—, pero con el contrato pasando por un archivo en medio. Si el RealPaymentGateway devolviera {"ok": False} o un monto distinto, la línea diría [FALLA] en vez de [OK ] —el mismo rojo de la lección 5, cazando una divergencia—. Insisto en lo importante: esto es una ilustración del concepto con la stdlib; Pact de verdad hace esto por HTTP, con un mock server real, un broker y muchísimos detalles más. Lo que quería que vieras es que la idea, despojada de la herramienta, es la que ya tienes.

Cuándo vale la pena Pact (y cuándo no)

Pact resuelve un problema específico y real: el despliegue independiente de servicios que se hablan. Cuando el equipo del PaymentGateway puede cambiar y desplegar su servicio sin coordinar con el equipo de BookingService, hay un riesgo permanente de que un cambio del provider rompa al consumer en producción, tarde y caro. Pact ataca justo eso: el pact file en el broker le dice al provider, antes de desplegar, si su cambio rompe a algún consumer conocido —hay incluso una función, can-i-deploy, que responde esa pregunta con un sí o un no—. Para arquitecturas de microservicios con varios equipos, ese seguro vale mucho.

Pero no es gratis, y no siempre hace falta. Pact añade infraestructura (el broker), una curva de aprendizaje (la API de la herramienta, el flujo de publicación y verificación en CI) y mantenimiento. Para dos componentes que viven en el mismo proceso —como BookingService y el repositorio de Reservo—, es exagerado: tu batería parametrizada hecha a mano les da la misma garantía sin instalar nada, porque no hay red que cruzar ni servicios que desplegar por separado. La regla práctica: si la costura es una llamada a un método dentro de un proceso, un contrato a mano (batería parametrizada) basta y sobra; si la costura es HTTP entre servicios desplegados por equipos distintos, es cuando Pact empieza a pagar su costo. Esta guía te enseña el patrón universal —el contrato consumer-driven— con la herramienta más simple que lo demuestra; saber que Pact existe y qué problema resuelve te deja elegir con criterio el día que la costura se vuelva de red.

Y una honestidad final sobre la frontera de esta guía: no instalamos Pact ni montamos un broker porque probar servicios de red de verdad, con un framework web y HTTP de punta a punta, es territorio de testing-backend-applications-guide. Aquí trabajamos la idea del contrato entre servicios sobre lo que ya tenemos —objetos en proceso y, a lo sumo, un http.server mínimo de la stdlib—. El concepto es transferible; la herramienta la aprendes cuando el proyecto la pida.

Errores comunes

Creer que Pact "prueba la integración" de los dos servicios juntos. Qué pasa: alguien piensa que Pact levanta ambos servicios y los conecta de verdad. Por qué pasa: "contract testing entre servicios" suena a "probarlos juntos". Cómo detectarlo: en Pact, el consumer se prueba contra un mock del provider, y el provider se verifica contra el pact file —los dos servicios nunca corren juntos—. Cómo corregirlo: Pact verifica que ambos cumplen el mismo contrato, cada uno por su lado, sin un entorno donde ambos vivan; eso es su gracia (rápido, sin desplegar todo) y su límite (no reemplaza una prueba de integración de punta a punta, que sí los junta —módulos 5 a 7—).

Instalar Pact para componentes del mismo proceso. Qué pasa: entusiasmado con el concepto, alguien mete Pact para el contrato entre BookingService y el repositorio, que viven en la misma memoria. Por qué pasa: la herramienta parece "la forma correcta y profesional". Cómo detectarlo: si no hay red entre los dos lados —es una llamada a un método—, el pact file y el broker no aportan nada que tu batería parametrizada no dé, y sí añaden peso. Cómo corregirlo: reserva Pact para costuras de red entre servicios desplegables por separado. En proceso, el contrato a mano es la herramienta correcta; no todo contrato necesita Pact.

Confundir el pact file con documentación que se escribe a mano. Qué pasa: alguien redacta el JSON del pact file a mano y lo mantiene como si fuera un doc. Por qué pasa: el archivo es legible y parece editable. Cómo detectarlo: si tu pact file y el comportamiento real del consumer pueden divergir (porque uno se edita a mano y el otro cambia en el código), perdiste la garantía. Cómo corregirlo: en Pact, el pact file lo genera el test del consumer, no una persona —así el contrato siempre refleja lo que el consumer de verdad hace—. El JSON de esta lección es ilustrativo para que lo leas; en un proyecto real, lo produce el test, no el teclado.

Ejercicios

Ejercicio 1 — Mapea las piezas. Para cada pieza de Pact, di a qué la equivale en el contrato a mano que construiste en este módulo: (a) el test del consumer que genera el pact file; (b) el pact file; (c) la verificación del provider; (d) el broker.

Ver solución
  • (a) El test del consumer que genera el pact file ↔ definir las cláusulas desde las necesidades del consumer (lección 3). En ambos, es el consumer quien declara qué espera; en Pact eso se graba a un archivo, en tu batería son funciones de pytest.
  • (b) El pact file ↔ el contrato compartido, la batería como spec único (lección 4). Es el acuerdo escrito; en Pact serializado a JSON para cruzar la red, en tu versión el texto de los tests parametrizados.
  • (c) La verificación del provider ↔ correr la batería contra una implementación (lecciones 4 y 5). En ambos, se comprueba que un provider cumple cada cláusula; en Pact la petición viaja por HTTP, en tu batería es una llamada a un método.
  • (d) El brokerno tiene equivalente en tu versión a mano, y esa ausencia es reveladora: el broker existe solo porque los dos servicios están separados y el contrato tiene que viajar y custodiarse. En proceso, el contrato no viaja a ningún lado, así que no hay broker. La red es lo que crea la necesidad del broker.

El ejercicio muestra la tesis de la lección: Pact = tu contrato consumer-driven + lo que la red obliga a añadir (serializar el contrato en un pact file, y un broker para compartirlo).

Ejercicio 2 — ¿Pact o batería a mano? Para cada costura, decide si conviene Pact o un contrato a mano (batería parametrizada), y por qué: (a) BookingServiceBookingRepository, ambos en el mismo proceso de Python; (b) BookingService (un servicio) ↔ PaymentGateway (un servicio HTTP de otro equipo, desplegado por separado); (c) dos funciones puras en el mismo módulo.

Ver solución
  • (a) Contrato a mano. Los dos lados viven en el mismo proceso; la costura es una llamada a un método. Tu batería parametrizada les da la garantía completa sin red, sin pact file, sin broker. Pact aquí sería peso muerto. Es exactamente el caso de todo este módulo.
  • (b) Pact. Aquí sí paga su costo: dos servicios separados, por HTTP, desplegados por equipos distintos que pueden cambiar sin coordinarse. El pact file y el broker le dicen al equipo del PaymentGateway, antes de desplegar, si su cambio rompe a BookingService. Este es el problema para el que Pact fue hecho.
  • (c) Ni uno ni otro (o casi). Dos funciones puras en el mismo módulo no tienen una costura de colaboración interesante: se prueban con unit tests directos (dado X, devuelve Y). No hay dos implementaciones intercambiables que puedan divergir, así que no hay contrato que compartir. Un contrato tiene sentido cuando hay una costura con al menos dos implementaciones posibles.

La regla que consolidas: el contrato a mano cubre las costuras en proceso; Pact cubre las costuras de red entre servicios independientes; y donde no hay costura de colaboración, ninguno de los dos —basta un unit test—.

Ejercicio 3 — La divergencia entre servicios. El equipo del PaymentGateway cambia su respuesta: donde antes devolvía {"ok": true, "amount_cents": 6000}, ahora devuelve {"success": true, "amount_cents": 6000} (renombró ok a success). BookingService lee receipt["ok"]. Explica qué pasaría sin Pact y cómo Pact lo frenaría, conectándolo con la divergencia del módulo 2.

Ver solución

Sin Pact, es el módulo 2 a escala de red, y peor. El equipo del gateway despliega su cambio creyéndolo inofensivo (solo renombró un campo). BookingService, que en producción lee receipt["ok"], empieza a recibir respuestas sin la clave ok y estalla —KeyError: 'ok'— en cada cobro, en producción, sin que ningún test del gateway ni del consumer lo hubiera avisado, porque cada equipo probó su lado por separado. Es la divergencia del módulo 2 —dos lados de una costura que dejaron de estar de acuerdo— agravada porque ahora los lados son servicios independientes y el consumer ni se enteró del cambio.

Pact lo frena en la verificación del provider. El pact file de BookingService declara que espera una response con ok: true. Cuando el equipo del gateway corre la verificación de su nuevo código contra ese pact file (en su CI, antes de desplegar), la interacción falla: la respuesta real trae success en vez de ok, no coincide con lo esperado, y la verificación se pone en rojo —[FALLA], como en la ilustración—. El equipo del gateway ve, antes de desplegar, que su cambio rompe a un consumer conocido. Es la misma cura de la lección 5 —una divergencia cazada en rojo antes de producción—, ahora operando a través de la red gracias al pact file y al broker que llevan el contrato del consumer hasta el CI del provider. El concepto es idéntico; Pact solo lo hace posible cuando los dos lados ya no comparten proceso.

Resumen y siguiente paso

En esta lección situaste todo lo que construiste dentro del panorama de la industria. Pact es la versión en red y automatizada del contrato consumer-driven que hiciste a mano: mismo patrón —el consumer define, el provider cumple—, con dos piezas que la red obliga a añadir, el pact file (el contrato serializado a JSON, capaz de cruzar entre dos bases de código) y el broker (el lugar central que lo custodia y lo comparte). Mapeaste cada pieza a lo tuyo —el test del consumer ↔ tus cláusulas, el pact file ↔ tu batería compartida, la verificación del provider ↔ correr la batería contra una implementación— y viste que el broker no tiene equivalente a mano porque solo existe cuando los servicios se separan. Con el contrato notariado entre empresas entendiste por qué la distancia (la red) obliga a escribir y custodiar el trato, y con una ilustración de stdlib viste el flujo entero —consumer genera, provider verifica— funcionar de verdad. Y fijaste el criterio: contrato a mano para costuras en proceso, Pact para costuras de red entre servicios independientes.

Antes de avanzar deberías poder: nombrar las tres piezas de Pact y su equivalente en tu contrato a mano; explicar por qué el pact file y el broker aparecen solo cuando la costura es de red; y decidir, ante una costura dada, si conviene un contrato a mano o Pact.

Con esto cierras la parte conceptual del módulo. Solo queda ponerlo todo junto con tus propias manos. En la lección 8, el mini-proyecto, escribes el contrato del BookingRepository como batería parametrizada desde cero, lo corres contra el Fake y el Sqlite —ambos verdes—, y luego metes el fake divergente para verlo cazado en rojo. Es la síntesis práctica de las siete lecciones, y tu entrega del módulo.

Recursos