Módulo 4: Verificar el contrato desde ambos lados
7. Quién posee el contrato: consumer-driven
Descripción
Las lecciones 5 y 6 dejaron una pregunta sin responder, y es la más importante de todas. Cuando el provider hace una cosa y el consumer espera otra —cuando get deja de lanzar, o cuando find_by_room no viene ordenado—, ¿quién tiene razón? ¿Quién define qué está prometido? No es una pregunta filosófica: es la que decide, en cada conflicto, si el rojo señala un bug del provider o una suposición de más del consumer. Y la respuesta tiene nombre propio en la industria: el contrato es consumer-driven, "manejado por el consumer". El dueño del contrato es quien lo consume, no quien lo implementa.
Esto puede sonar al revés. La intuición dice que el que construye el repositorio —el provider— debería decidir cómo se comporta. Pero piénsalo desde la razón de ser del componente: SqliteBookingRepository no existe para sí mismo; existe para que BookingService lo use. Un método que nadie llama no necesita existir; un comportamiento que ningún consumer necesita no necesita prometerse. Por eso las necesidades del consumer son las cláusulas del contrato: el consumer necesita guardar-y-leer, así que esa es una cláusula; necesita que get lance para poder distinguir el id ausente, así que esa es otra. El provider no inventa qué prometer; promete cumplir lo que el consumer necesita. Esta lección hace explícita esa propiedad —el consumer posee el contrato— y muestra cómo se automatiza entre servicios en el concepto de Pact, la herramienta que llevó esta idea a la red sin que tú tengas que instalarla para entenderla.
Conexión con el módulo: esta es la lección de gobierno, la que da el marco a todo lo anterior. Las lecciones 2 y 3 te dieron los dos lados; la 4, la garantía de correrlos juntos; las 5 y 6, las dos formas de romper el acuerdo. Esta responde quién manda en el acuerdo, y con eso cierra el arco conceptual del módulo. La lección 8 lo pone en práctica en el mini-proyecto. Después, el módulo 5 abandona el contrato aislado para probar los componentes reales juntos —la integración de verdad—.
Analogía: el cliente que encarga el traje
Imagina un sastre y un cliente. El cliente va a encargar un traje para una boda: necesita que le quede bien a él, con sus medidas, para esa ocasión. ¿Quién define las especificaciones del traje —el largo de la manga, el ancho de la espalda, el color? El cliente, porque el traje existe para servirle a él. El sastre no decide "voy a hacer las mangas de este largo porque me gusta"; toma las medidas del cliente y promete cumplirlas. El cliente maneja la especificación; el sastre la ejecuta. Es customer-driven, por así decirlo.
Ahora, algo sutil pero decisivo: la especificación se define a partir de lo que el cliente de verdad necesita, no de todo lo que el sastre podría hacer. Si el sastre añade, por su cuenta, un bolsillo secreto que el cliente nunca pidió, ese bolsillo no es parte del acuerdo —y si un día lo quita, el cliente no puede quejarse, porque nunca lo encargó—. Al revés: si el cliente da por hecho que el traje trae un pañuelo a juego, pero nunca lo especificó, y el traje llega sin pañuelo, la culpa es del cliente por suponer de más, no del sastre. La especificación es exactamente lo que el cliente pidió: ni los regalos no pedidos del sastre, ni las suposiciones tácitas del cliente.
BookingService es el cliente; SqliteBookingRepository es el sastre; el contrato es la especificación del traje. Las cláusulas son las medidas que el consumer necesita —guardar-y-leer, get que lanza, filtrar por sala—. Un comportamiento que el provider tiene pero que ningún consumer pidió (el orden de inserción, la identidad del objeto) es el bolsillo secreto: no es parte del acuerdo, y apoyarse en él es suponer de más (lección 6). Un comportamiento que el consumer necesita pero que el contrato no verifica es el pañuelo dado por hecho: hay que escribirlo en la especificación o dejar de esperarlo. El dueño de la especificación es el cliente, porque el traje —y el repositorio— existen para servirle.
Ejemplo trabajado: el consumer define, el provider verifica
"Consumer-driven" no es solo una idea; tiene una forma concreta en cómo escribes y corres los tests. El flujo tiene dos mitades que ya construiste por separado, y verlas juntas revela la propiedad. Primera mitad: el consumer define sus expectativas. El test del consumer (lección 2) es donde BookingService declara qué necesita del repositorio: necesita que guardar-y-leer funcione, y necesita que get lance para un id ausente. Esas expectativas son el contrato en germen. Segunda mitad: el provider las verifica. El test del provider (lección 3) toma esas mismas expectativas —convertidas en cláusulas— y comprueba que el SqliteBookingRepository real las cumple.
Corramos las dos mitades juntas: las expectativas del consumer arriba, la verificación del provider abajo. Es el flujo consumer-driven completo, en proceso.
Qué esperar. En mi máquina (Python 3.14.0, pytest 9.1.1):
python3 -m pytest tests/test_consumer_bookingservice.py tests/test_repository_contract.py -v -k "consumer or sqlite"
============================= test session starts ==============================
platform darwin -- Python 3.14.0, pytest-9.1.1, pluggy-1.6.0
collected 10 items / 4 deselected / 6 selected
tests/test_consumer_bookingservice.py::test_book_persists_a_retrievable_booking PASSED [ 16%]
tests/test_consumer_bookingservice.py::test_cancel_of_a_missing_booking_propagates_the_contract_error PASSED [ 33%]
tests/test_repository_contract.py::test_save_then_get_returns_the_same_booking[sqlite] PASSED [ 50%]
tests/test_repository_contract.py::test_get_of_a_missing_id_raises[sqlite] PASSED [ 66%]
tests/test_repository_contract.py::test_saving_the_same_id_twice_updates_not_duplicates[sqlite] PASSED [ 83%]
tests/test_repository_contract.py::test_find_by_room_returns_only_that_rooms_bookings[sqlite] PASSED [100%]
======================= 6 passed, 4 deselected in 0.01s ========================
Léelo de arriba abajo como el flujo consumer-driven. Las dos primeras líneas son las expectativas del consumer: BookingService declara, en verde, "necesito guardar-y-recuperar" y "necesito que cancel de un id ausente propague el error del contrato" —o sea, "necesito que get lance"—. Las cuatro de abajo son la verificación del provider: el SqliteBookingRepository real cumple cada cláusula que nace de esas necesidades. Fíjate en el vínculo directo: la expectativa del consumer test_cancel_of_a_missing_booking_propagates_the_contract_error (arriba) y la cláusula del provider test_get_of_a_missing_id_raises[sqlite] (abajo) son la misma promesa vista desde los dos dueños: el consumer la pide, el provider la cumple. El contrato no cayó del cielo ni lo dictó el provider: brotó de lo que el consumer necesita, y el provider se verificó contra ello. Eso es consumer-driven hecho salida de pytest.
Por qué el dueño es el consumer y no el provider
Detengámonos en el argumento, porque invertir la intuición correctamente es el corazón de la lección. Hay tres razones por las que el contrato lo posee el consumer.
Porque el componente existe para el consumer. Un provider es un medio, no un fin: SqliteBookingRepository existe para que BookingService persista reservas. Sus promesas se justifican por las necesidades que sirven. Si mañana ningún consumer necesitara find_by_room, esa promesa podría desaparecer sin que nadie la extrañara. Las promesas nacen de la demanda, y la demanda la pone el consumer.
Porque el consumer es quien sufre si se rompen. Cuando get deja de lanzar, el que se rompe es cancel —el consumer—. El provider "funciona" (devuelve None sin error); es el consumer quien paga el precio. Quien sufre el incumplimiento tiene la autoridad natural para definir qué constituye cumplimiento. El sastre no sufre si la manga queda corta; el cliente sí, y por eso el cliente define el largo.
Porque evita prometer de más. Si el provider definiera el contrato, la tentación sería prometer todo lo que hace —el orden de inserción, la identidad del objeto, cada detalle incidental—, y cada uno de esos detalles se volvería una promesa que ata a toda implementación futura. Al dejar que el consumer defina, el contrato contiene solo lo que alguien de verdad necesita, y queda mínimo. Un contrato mínimo deja al provider libre para cambiar todo lo demás sin romper a nadie. Consumer-driven no es solo "quién manda"; es la disciplina que mantiene el contrato pequeño y, por tanto, el provider flexible.
De aquí sale la resolución de los conflictos de las lecciones 5 y 6. Si el consumer necesita algo y el contrato lo promete, el provider debe cumplirlo (romperlo es un breaking change: lección 5). Si el consumer supone algo que el contrato no promete, el consumer está equivocado (suposición de más: lección 6). El contrato —definido por lo que el consumer escribió que necesita, no por lo que tácitamente dio por hecho— es el árbitro. Y por eso escribir bien las expectativas del consumer importa tanto: son la ley.
El concepto de Pact: consumer-driven entre servicios, por la red
Todo lo que hiciste a mano tiene una encarnación industrial para cuando los dos lados no están en el mismo proceso, sino en servicios separados que se hablan por HTTP: el consumer es un servicio, el provider es otro, corren en máquinas distintas, los mantienen equipos distintos. Ahí no puedes simplemente importar el fake del otro y correr una batería compartida. Pact es la herramienta que lleva la idea consumer-driven a ese mundo. Vale la pena entender su forma, porque es exactamente el patrón de este módulo, estirado por la red.
El flujo de Pact, conceptualmente:
-
El consumer genera el contrato. En su suite, el consumer corre sus tests contra un mock provider que Pact levanta —un stand-in que responde según lo que el consumer espera, igual que el fake fue nuestro stand-in en la lección 2—. Al correr, Pact registra cada interacción esperada ("cuando pido
GET /bookings/xyzde un id ausente, espero un404") y las escribe en un pact file: un JSON con las expectativas del consumer. El contrato lo produce el consumer, a partir de sus propios tests. Eso es consumer-driven literal: el artefacto del contrato nace del lado que consume. -
Un broker comparte el contrato. El pact file se publica en un Pact Broker, un servicio central donde viven los contratos. El broker es el punto de encuentro entre equipos que no comparten código: el consumer publica ahí sus expectativas, el provider las recoge de ahí.
-
El provider se verifica contra el contrato. El equipo del provider toma el pact file del broker y corre la verificación del provider: reproduce cada interacción registrada contra su implementación real y comprueba que responde como el consumer espera. Es exactamente el test del provider de la lección 3 —"dado X, devuelvo Y"—, solo que la X y la Y vienen del pact file que el consumer generó, no de una batería escrita a mano en el mismo repo.
-
Una puerta antes del deploy. El broker puede responder a la pregunta "¿puedo desplegar esta versión sin romper a nadie?" —la función Can I Deploy— cruzando qué consumers dependen de qué versiones del provider verificadas. Es la versión en red de "corre el contrato antes de fusionar" de la lección 5: el breaking change se caza entre servicios antes del deploy, no solo dentro de un proceso.
La correspondencia con lo que hiciste es uno a uno. Nuestro FakeBookingRepository es el mock provider de Pact. Nuestra batería compartida es el pact file. Correr la batería contra el SqliteBookingRepository es la verificación del provider. Y correr el contrato antes de fusionar es el Can I Deploy. La diferencia es solo el medio: nosotros estamos en un proceso, con un import; Pact está entre servicios, con un JSON y un broker. Por eso esta guía enseña la idea a fondo y no instala la herramienta: una vez que entiendes los dos lados, la batería compartida y la verificación, Pact es ese mismo esqueleto vestido para la red. Cuando lo veas en un sistema real, reconocerás cada hueso.
Cuándo esto vale la pena (y cuándo no)
Consumer-driven y Pact resuelven un problema específico: dos componentes que evolucionan por separado y deben seguir hablándose. El valor crece con la distancia entre los lados. Vale mucho la pena cuando el consumer y el provider son servicios distintos, en repos distintos, mantenidos por equipos distintos que despliegan en momentos distintos —ahí un breaking change es invisible hasta que estalla en integración, y el contrato es la única red—. Vale la pena, en su versión a mano (la de este módulo), cuando tienes una costura con varias implementaciones (el fake y el real) que deben coincidir. Y vale menos la pena cuando los dos lados viven en el mismo módulo, cambian juntos en el mismo commit y se prueban juntos: ahí un test de integración directo puede bastar, y montar el aparato de contrato sería sobre-ingeniería.
La regla: el contract testing paga cuando el costo de un breaking change no detectado es alto y su detección natural es tardía (servicios separados, despliegues independientes). Si tus dos lados siempre cambian y se prueban juntos, el contrato aporta menos que una integración directa —que es justo lo que el módulo 5 empieza a construir—.
Errores comunes
Dejar que el provider defina el contrato "porque es quien sabe cómo funciona". Qué pasa: el equipo del provider escribe el contrato a partir de todo lo que su implementación hace, y el consumer se adapta a eso. Por qué pasa: el provider parece la autoridad sobre su propio comportamiento. Cómo detectarlo: si el contrato incluye promesas que ningún consumer usa (el orden de inserción, detalles internos), está manejado por el provider, no por el consumer. Cómo corregirlo: invierte la dirección. Que el contrato nazca de lo que los consumers necesitan —de sus tests de expectativas—, no de lo que el provider hace. Así queda mínimo y no ata al provider con promesas incidentales que un día querrá cambiar.
Confundir el pact file (el artefacto) con el concepto. Qué pasa: alguien cree que "hacer contract testing" es obligatoriamente generar un JSON con Pact y montar un broker. Por qué pasa: se toma la herramienta por la idea. Cómo detectarlo: si piensas que sin Pact instalado no hay contract testing, confundes el vehículo con el viaje. Cómo corregirlo: el concepto es "el consumer define expectativas, el provider las verifica, y ambos se prueban contra el mismo acuerdo". La batería parametrizada de este módulo es contract testing, sin un solo JSON. Pact aporta el pact file y el broker cuando los lados están separados por la red; en proceso, no hacen falta. Entiende la idea primero; la herramienta es opcional y depende del contexto.
Tratar el contrato como intocable en vez de como un acuerdo vivo. Qué pasa: cuando el consumer necesita algo nuevo, alguien lo agrega como suposición tácita en vez de cambiar el contrato, "para no tocar la batería". Por qué pasa: modificar el contrato se siente pesado. Cómo detectarlo: si el consumer depende de comportamientos que no están en ninguna cláusula, el contrato dejó de reflejar lo que el consumer de verdad necesita —está desactualizado—. Cómo corregirlo: el contrato es consumer-driven y por tanto vivo: cuando las necesidades del consumer cambian, se cambia el contrato (una cláusula nueva, verificada contra ambos lados) a la par. Un contrato que no se actualiza con las necesidades del consumer deja de ser el árbitro fiable de las lecciones 5 y 6, y las suposiciones de más se cuelan por el hueco entre lo que el consumer necesita y lo que el contrato dice.
Ejercicios
Ejercicio 1 — Traza el vínculo. En la salida del ejemplo trabajado, la línea del consumer test_cancel_of_a_missing_booking_propagates_the_contract_error y la del provider test_get_of_a_missing_id_raises[sqlite] están conectadas. Explica cómo se corresponden en el flujo consumer-driven, y quién "posee" esa promesa concreta.
Ver solución
Las dos líneas son la misma promesa vista desde sus dos dueños de tarea:
- La del consumer (
test_cancel_..._propagates_the_contract_error) es dondeBookingServicedeclara su necesidad: "yo, cuando cancelo un id que no existe, necesito que el error se propague; es decir, necesito quegetlance". Esa es la expectativa del consumer, la que define la cláusula. - La del provider (
test_get_of_a_missing_id_raises[sqlite]) es donde elSqliteBookingRepositoryverifica que cumple esa necesidad: "dado un id ausente, yo lanzoKeyError".
Quién posee la promesa: el consumer. La promesa "get lanza para un id ausente" existe porque cancel la necesita para funcionar; no es un capricho del repositorio. Si cancel no existiera y ningún otro consumer necesitara distinguir el id ausente por una excepción, esta cláusula no tendría razón de estar en el contrato. El consumer la definió con su necesidad; el provider solo promete cumplirla. Por eso, cuando el provider la rompió (lección 5), el rojo fue legítimo: el provider incumplió una promesa que el consumer tenía derecho a exigir. La dirección de la propiedad —del consumer al provider— es la que hace que ese rojo signifique "el provider falló" y no "el consumer es exigente".
Ejercicio 2 — Del fake al pact file. Establece la correspondencia entre nuestra versión a mano y el flujo de Pact, para cada pieza: (a) el FakeBookingRepository; (b) la batería de contrato compartida; (c) correr la batería contra SqliteBookingRepository; (d) correr el contrato antes de fusionar un cambio del provider.
Ver solución
- (a) El
FakeBookingRepository↔ el mock provider de Pact. Ambos son el stand-in contra el que se prueba el consumer: un doble que responde según el contrato, sin la implementación real. En proceso es un objeto que importas; en Pact es un servidor de mentira que la herramienta levanta y contra el que el consumer manda sus peticiones reales. - (b) La batería de contrato compartida ↔ el pact file. Ambos son el artefacto que captura el acuerdo: las cláusulas que ambos lados deben cumplir. En proceso es un archivo de tests parametrizado; en Pact es un JSON con las interacciones esperadas, generado por el consumer.
- (c) Correr la batería contra
SqliteBookingRepository↔ la verificación del provider. Ambos son "el provider real rinde cuentas contra el acuerdo": tomar las expectativas y comprobar que la implementación las cumple. En proceso es-k sqlite; en Pact es la tarea de verificación que reproduce el pact file contra el provider. - (d) Correr el contrato antes de fusionar ↔ el Can I Deploy del broker. Ambos son la puerta que impide que un breaking change llegue a producción: verificar la compatibilidad antes del deploy. En proceso es correr la batería en tu máquina o CI; en Pact es preguntarle al broker si la versión que quieres desplegar es compatible con los consumers verificados.
El patrón es idéntico en las cuatro; solo cambia el medio (un proceso con imports frente a servicios con JSON y broker). Entender la columna de la izquierda es entender la de la derecha.
Ejercicio 3 — ¿Consumer-driven o integración directa? Para cada escenario, decide si conviene un contrato consumer-driven (a lo Pact) o basta un test de integración directo, y por qué: (a) el equipo de facturación (otro servicio, otro repo, otro despliegue) consume tu API de reservas por HTTP; (b) BookingService y SqliteBookingRepository viven en el mismo módulo de Reservo y siempre se despliegan juntos.
Ver solución
- (a) Servicio de facturación externo → contrato consumer-driven (Pact). Los lados están separados por la red, en repos y despliegues distintos, mantenidos por equipos distintos. Un breaking change en tu API sería invisible para ti hasta que el servicio de facturación se rompiera en integración —tarde y caro—. Aquí el contrato consumer-driven es la red que hace falta: facturación publica sus expectativas como pact file, tú verificas tu API contra ellas antes de desplegar, y el Can I Deploy impide romperlos. La distancia entre los lados es exactamente lo que justifica el aparato.
- (b)
BookingServiceySqliteBookingRepositoryen el mismo módulo → integración directa (probablemente). Los dos lados cambian juntos, en el mismo commit, y se prueban juntos. El costo de un breaking change es bajo porque se detecta de inmediato al correr la suite del módulo, y montar el aparato de pact file + broker sería sobre-ingeniería. Un test de integración que useBookingServicecon elSqliteBookingRepositoryreal (el módulo 5) cubre la costura sin la maquinaria de contratos entre servicios. (Dicho esto, la batería de contrato a mano de este módulo sigue siendo útil aquí para mantener honesto al fake frente al real —eso no es sobre-ingeniería, es barato—; lo que sería excesivo es el JSON y el broker de Pact para dos piezas que viven juntas.)
La regla que decide: ¿cuánta distancia hay entre los lados? Mucha (servicios, repos, equipos, despliegues separados) → contrato consumer-driven formal. Poca (mismo módulo, mismo despliegue) → integración directa, quizás con la batería de contrato a mano para el fake, pero sin el aparato completo.
Resumen y siguiente paso
En esta lección respondiste la pregunta de gobierno del módulo: el consumer posee el contrato. El componente existe para servir al consumer, el consumer es quien sufre si las promesas se rompen, y dejar que él las defina mantiene el contrato mínimo y al provider flexible. Con el cliente que encarga el traje entendiste que la especificación la define quien la necesita —ni los regalos no pedidos del sastre, ni las suposiciones tácitas del cliente—, y viste el flujo consumer-driven en salida real: las expectativas del consumer arriba, la verificación del provider abajo, la misma promesa vista desde sus dos dueños. Y mapeaste todo tu trabajo a mano al concepto de Pact —el fake es el mock provider, la batería es el pact file, correr contra SQLite es la verificación del provider, correr antes de fusionar es el Can I Deploy—, entendiendo que Pact es este mismo esqueleto vestido para la red, y por qué esta guía enseña la idea sin instalar la herramienta.
Antes de avanzar deberías poder: argumentar por qué el dueño del contrato es el consumer y no el provider; usar esa propiedad para resolver los conflictos de las lecciones 5 y 6 (promesa rota contra suposición de más); mapear las cuatro piezas de tu versión a mano al flujo de Pact; y decidir cuándo un contrato consumer-driven paga y cuándo basta una integración directa.
Con esta lección cierras el arco conceptual del módulo: los dos lados, la batería compartida, las dos formas de romperlo, y quién manda. La lección 8 lo pone todo en tus manos en el mini-proyecto —verificar el contrato desde ambos lados y cazar un breaking change que tú introduces—. Y después, el módulo 5 da el siguiente paso natural: dejar el contrato del repositorio aislado y probar los componentes reales juntos, cruzando la costura de verdad. El contrato garantizó que el fake no miente; la integración verifica que las piezas reales encajan.
Recursos
- docs.pact.io — Introducción y How Pact works — la referencia oficial del contract testing consumer-driven: el mock provider, el pact file, el broker y la verificación del provider; el mapa completo de la versión industrial de este módulo.
- docs.pact.io — Can I Deploy — la función del broker que decide si una versión puede desplegarse sin romper a sus consumers; la versión en red de "corre el contrato antes de fusionar" de la lección 5.
- Martin Fowler — Consumer-Driven Contracts — el artículo que nombró el patrón y explica por qué el consumer maneja el contrato; el fundamento conceptual de toda la lección.
- Documentación de pytest — Seleccionar tests con
-k— el selector con el que corrimos juntas las expectativas del consumer y la verificación del provider (-k "consumer or sqlite") para ver el flujo consumer-driven completo.