Módulo 4: Verificar el contrato desde ambos lados

2. El lado del consumer

Descripción

Empecemos por la primera silla: la del consumer, el componente que usa al colaborador. En la costura del repositorio, el consumer es BookingService, y su punto de vista se resume en una frase que vas a repetir todo el módulo: "yo mando X y espero recibir Y". Yo, BookingService, guardo una reserva y espero poder leerla de vuelta. Yo pido una reserva por un id que no existe y espero que me lo digan lanzando, no devolviendo None en silencio. El test del consumer es la verificación de esas expectativas: comprueba que el consumer funciona correctamente cuando el otro lado cumple el contrato.

Y trae una pregunta más filosa, que es lo que de verdad enseña esta lección. El test del consumer no solo verifica que BookingService hace su trabajo; verifica que lo hace apoyándose solo en lo que el contrato promete, y en nada más. Un consumer puede funcionar por accidente gracias a un detalle que su provider actual tiene pero que el contrato nunca garantizó —un orden, un tipo, un efecto colateral—. Ese consumer está frágil: el día que lo conectes a otro provider igualmente válido, se rompe. El test del consumer bien hecho ejercita al consumer contra un provider que promete exactamente el contrato, ni un ápice más, para que si el consumer supone de más, se note. Escribir el consumer contra el contrato, no contra una implementación concreta, es la disciplina que esta lección instala.

Conexión con el módulo: la lección 1 te mostró que el contrato tiene dos lados; esta se sienta en el primero. Aquí verás cómo se escribe y corre un test del consumer, por qué corre contra el fake (que honra el contrato) y no contra SQLite, y qué significa "apoyarse solo en lo prometido". La lección 3 se cambiará a la silla del provider —la pregunta espejo, "dado X, devuelvo Y"—. Y la lección 6 tomará el filo de esta —"sin suponer de más"— y lo convertirá en su propio pago: cazar una suposición de más del consumer. Por ahora, aprende a ver la costura desde la silla del que llama.

Analogía: el pedido en la cocina

Piensa en un mesero que toma pedidos en un restaurante. El mesero es el consumer: usa a la cocina sin cocinar. Su trabajo, del lado que le toca, es mandar comandas claras y confiar en lo que el acuerdo con la cocina promete: "si entrego una comanda con el plato y la mesa, la cocina me devuelve ese plato listo, o me avisa explícitamente si el ingrediente se acabó". Ese acuerdo es el contrato. Un buen mesero se apoya solo en él: manda la comanda, espera el plato o el aviso, y actúa en consecuencia.

Ahora imagina un mesero que, sin darse cuenta, se apoya en algo que el acuerdo no promete. En su cocina de siempre, el cocinero resulta ser zurdo y deja los platos listos en el extremo izquierdo de la barra, así que el mesero se acostumbró a mirar solo a la izquierda. Funciona —durante años— porque su cocinero siempre pone los platos ahí. Pero el acuerdo nunca dijo "los platos salen por la izquierda"; eso es un detalle del cocinero actual, no del contrato. El día que entra un cocinero diestro que deja los platos a la derecha, el mesero se queda parado mirando una barra izquierda vacía, jurando que la cocina falló —cuando la cocina cumplió el acuerdo al pie de la letra, y fue él quien se apoyó en un detalle no prometido—.

El test del consumer bien hecho es ensayar al mesero contra una cocina que cumple el acuerdo y solo el acuerdo: entrega los platos, pero no siempre por el mismo lado, no siempre en el mismo orden, sin ningún regalo que el contrato no obligue. Si el mesero funciona contra esa cocina "estricta", funcionará contra cualquier cocinero legal. Si se apoyaba en la izquierda, el ensayo lo delata antes del servicio. Verificar al consumer contra el contrato —no contra la comodidad de su provider de siempre— es lo que lo hace robusto.

Ejemplo trabajado: el consumer contra un provider que honra el contrato

Aquí está el test del consumer. Fíjate en la elección deliberada: el provider es el FakeBookingRepository, no SQLite. ¿Por qué el fake? Porque el fake honra el contrato —esa es la razón de su existencia desde el módulo 3—, así que representa fielmente "un provider cualquiera que cumple lo prometido". Probar el consumer contra el fake es probarlo contra el contrato hecho objeto, sin arrastrar una base de datos. Todo lo demás en la costura —el pago, el correo, el reloj— también se dobla, porque no es lo que estamos probando: el foco es cómo BookingService usa el repositorio.

# tests/test_consumer_bookingservice.py — el consumer contra un provider que honra el contrato
from datetime import datetime

from reservo.calendar import Calendar
from reservo.doubles import (FakeBookingRepository, FixedClock,
                             SpyEmailSender, StubPaymentGateway)
from reservo.models import Member, Room
from reservo.services import BookingService

FOCUS = Room(id="focus", name="Focus", capacity=4, hourly_cents=2500)
ANA = Member(id="m-ana", name="Ana", tier="pro")
START = datetime(2026, 3, 10, 9)
END = datetime(2026, 3, 10, 12)          # Focus 3 h
CLOCK = datetime(2026, 3, 1, 9)


def make_service(repo):
    return BookingService(
        Calendar(), FixedClock(CLOCK),
        StubPaymentGateway(ok=True), SpyEmailSender(), repo,
    )


# el consumer manda X (una reserva de Focus 3 h) y espera Y (queda guardada y recuperable)
def test_book_persists_a_retrievable_booking():
    repo = FakeBookingRepository()           # un provider que honra el contrato
    service = make_service(repo)

    booking = service.book(FOCUS, ANA, START, END)

    saved = repo.get(booking.id)             # el consumer solo usa save/get del contrato
    assert saved.status == "confirmed"
    assert saved.price_cents == 6000


# el consumer espera que get de un id ausente lance (la clausula 2 del contrato)
def test_cancel_of_a_missing_booking_propagates_the_contract_error():
    repo = FakeBookingRepository()
    service = make_service(repo)

    import pytest
    with pytest.raises(KeyError):            # el consumer se apoya en que get lanza
        service.cancel("does-not-exist")

Léelos como afirmaciones sobre el consumer, no sobre el repositorio. El primer test dice: "cuando BookingService.book corre contra un provider que cumple el contrato, la reserva queda guardada y se puede recuperar con el estado y el precio correctos". El segundo dice algo más sutil y más importante: "BookingService.cancel se apoya en la cláusula 2 del contrato —que get de un id ausente lanza—; si le pides cancelar una reserva que no existe, ese KeyError del provider se propaga". Ese segundo test documenta una dependencia del consumer sobre una promesa concreta del contrato: cancel confía en que get lanza. Guárdate ese hilo: es exactamente la promesa que el breaking change de la lección 5 romperá.

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

python3 -m pytest tests/test_consumer_bookingservice.py -v
============================= test session starts ==============================
platform darwin -- Python 3.14.0, pytest-9.1.1, pluggy-1.6.0
collected 2 items

tests/test_consumer_bookingservice.py::test_book_persists_a_retrievable_booking PASSED [ 50%]
tests/test_consumer_bookingservice.py::test_cancel_of_a_missing_booking_propagates_the_contract_error PASSED [100%]

============================== 2 passed in 0.01s ==============================

Dos verdes, sin una base de datos a la vista. El consumer quedó verificado —hace su trabajo cuando el provider cumple el contrato— y quedó documentado en qué promesas se apoya: en que save/get guardan y recuperan, y en que get lanza para un id ausente. Corrió en centésimas de segundo porque el provider es el fake, y el fake es fiel porque el contrato lo mantiene honesto. Ese es el trato de todo el módulo: como el fake honra el contrato, probar el consumer contra el fake es tan válido como probarlo contra el real, pero mil veces más rápido.

Qué verifica —y qué no— un test del consumer

Un test del consumer tiene un alcance preciso, y confundirlo con otra cosa es el error más común de esta lección. Fijemos sus límites.

Verifica la lógica del consumer, dando por cumplido el contrato del provider. El test del consumer asume que el provider cumple lo prometido —por eso usa el fake, que lo cumple— y sobre esa base comprueba que el consumer orquesta bien: que book guarda una reserva confirmada con el precio correcto, que cancel se apoya en que get lanza. No verifica que SQLite serialice bien un datetime; eso es trabajo del test del provider (lección 3). El reparto es limpio: el consumer supone el contrato cumplido y prueba lo suyo; el provider prueba que lo cumple.

No es un test de integración. No hay ninguna pieza real cruzando una costura de frontera: el repositorio es un fake en memoria, el pago un stub, el correo un spy. Todo corre en proceso, sin disco ni red. Si tu "test del consumer" abre sqlite3.connect(...), ya no estás probando el consumer contra el contrato: estás probando BookingService + SQLite juntos, que es integración —el módulo 5, no este—. La marca de un test del consumer es que el provider está doblado por algo que honra el contrato.

No prueba al provider real. Un test del consumer verde no dice nada sobre si SqliteBookingRepository cumple el contrato. Podrías tener el consumer impecable y un SQLite roto, y este test seguiría verde, porque nunca toca SQLite. Por eso el contrato necesita sus dos lados: el test del consumer cubre "el consumer usa bien el contrato", y solo el test del provider cubre "el provider real cumple el contrato". Ninguno reemplaza al otro.

La idea Pact: el consumer se prueba contra un stand-in que promete el contrato

Lo que acabas de hacer a mano tiene un nombre en la industria, y entenderlo te da el marco completo. En el contract testing consumer-driven —la idea que Pact automatiza entre servicios—, el test del consumer nunca corre contra el provider real. Corre contra un stand-in: un doble del provider que se programa para responder exactamente según el contrato, y que además registra las expectativas del consumer para producir el contrato mismo.

En el mundo de servicios por HTTP, ese stand-in es un servidor de mentira que Pact levanta: el consumer le manda sus peticiones reales, el stand-in responde según lo pactado, y Pact anota "el consumer espera que, ante esta petición, el provider responda así". El resultado es un pact file: un JSON con las expectativas del consumer, que más tarde el provider real usará para verificarse (eso es la lección 3 y la 7). La clave conceptual: el consumer se prueba contra una promesa, no contra una implementación.

En nuestra versión en proceso, el FakeBookingRepository es ese stand-in. Cumple el contrato y nada más, así que probar BookingService contra él es probarlo contra "un provider que promete el contrato". No generamos un pact file en JSON —no hace falta, estamos en un solo proceso—, pero la forma es idéntica: consumer contra un provider-que-honra-el-contrato, verificando que el consumer se apoya solo en lo prometido. Cuando en tu trabajo veas Pact levantar un mock server para el consumer test, reconocerás que es este mismo patrón, llevado a la red.

Errores comunes

Probar el consumer contra el provider real "para estar más seguro". Qué pasa: alguien cambia el fake por un SqliteBookingRepository en el test del consumer, pensando que probar contra lo real da más confianza. Por qué pasa: "más real" suena a "mejor test". Cómo detectarlo: el test se volvió más lento, abrió una conexión a la base de datos, y ahora puede fallar por algo que no es el consumer (el esquema, la serialización). Cómo corregirlo: separa las preguntas. "¿El consumer usa bien el contrato?" se responde contra el fake, rápido. "¿El provider real cumple el contrato?" se responde con el test del provider (lección 3). Mezclarlas en un solo test te quita la capacidad de saber cuál lado falló, y te lleva a integración sin querer.

Apoyar el consumer en un detalle no prometido y no notarlo. Qué pasa: BookingService funciona porque el fake devuelve las reservas en cierto orden, o porque get devuelve el mismo objeto que guardaste (identidad, no solo igualdad), y nadie se da cuenta de que el contrato no promete eso. Por qué pasa: el provider actual tiene ese detalle, así que el consumer funciona y el test pasa. Cómo detectarlo: pregúntate por cada cosa en que el consumer se apoya: "¿esto está en una cláusula del contrato, o es un regalo de este provider?". Cómo corregirlo: si es un regalo no prometido, o el consumer deja de depender de él, o se añade como cláusula explícita del contrato. La lección 6 es enteramente sobre cazar este error; por ahora, basta con sospecharlo.

Verificar el provider dentro del test del consumer. Qué pasa: en el test de book, alguien añade assert isinstance(saved.start, datetime) para "asegurarse de que se guardó bien". Por qué pasa: parece razonable verificar el tipo de lo que quedó. Cómo detectarlo: esa aserción habla del provider (cómo guarda y devuelve el start), no del consumer (cómo usa el repositorio). Contra el fake siempre pasará, sin decir nada del real. Cómo corregirlo: las promesas sobre lo que el provider devuelve —tipos, campos, errores— viven en la batería de contrato, que corre contra ambos providers (lección 4). El test del consumer se queda en lo suyo: que el consumer orquesta bien dado el contrato cumplido.

Ejercicios

Ejercicio 1 — La expectativa oculta de cancel. El test test_cancel_of_a_missing_booking_propagates_the_contract_error verifica que cancel de un id ausente lanza KeyError. Explica qué promesa del contrato está ejercitando ese test desde el lado del consumer, y por qué documentarla importa para lo que viene en la lección 5.

Ver solución

El test ejercita la cláusula 2 del contrato: "get de un id ausente lanza". Desde el lado del consumer, lo que se verifica es que BookingService.cancel se apoya en esa promesa: cancel empieza llamando self._repo.get(booking_id), y confía en que, si el id no existe, ese get lanza —lo que hace que cancel falle de inmediato con un KeyError claro, en vez de continuar como si la reserva existiera—. El test documenta esa dependencia: "el consumer necesita que get lance".

Por qué importa para la lección 5: el breaking change que veremos es precisamente que el provider SqliteBookingRepository.get deje de lanzar y devuelva None. Como este test del consumer dejó registrado que cancel depende de que get lance, tienes el mapa completo del daño: cuando el provider rompe esa promesa, no es un detalle abstracto —es exactamente la promesa de la que cancel cuelga—. El test del consumer explica por qué el breaking change del provider es peligroso: hay un consumer real apoyado en la promesa que se rompió.

Ejercicio 2 — ¿Por qué el fake y no SQLite? Un compañero propone reescribir los dos tests del consumer usando SqliteBookingRepository(sqlite3.connect(":memory:")) en vez del FakeBookingRepository, "para probar contra algo real". Da dos razones concretas por las que el fake es la elección correcta para un test del consumer, y di qué tipo de test sí querría el provider real.

Ver solución

Dos razones por las que el fake es lo correcto aquí:

  1. El test del consumer pregunta por el consumer, no por el provider. Su objetivo es "¿BookingService usa bien el contrato?". El fake honra el contrato, así que representa fielmente "un provider cualquiera que cumple lo prometido" —justo lo que el consumer debe suponer—. Cambiarlo por SQLite mezcla dos preguntas (¿el consumer usa bien? ¿el provider real cumple?) en un solo test, y si falla, ya no sabes cuál de las dos falló.
  2. Velocidad y determinismo. El fake corre en memoria, sin conexión, sin esquema, sin commit. Los tests del consumer suelen ser muchos (uno por cada camino de book, cancel, etc.); atarlos todos a SQLite los vuelve lentos y frágiles frente a un problema que no es del consumer. La pirámide (guía hermana) quiere justo esto: muchos tests rápidos del consumer contra el fake.

Qué test sí querría el provider real: uno del lado del provider (lección 3), que ejercite SqliteBookingRepository contra el contrato directamente —"guardar-y-leer devuelve la misma reserva", "get ausente lanza"—, sin BookingService de por medio. Ese es el lugar correcto para tocar SQLite: probar que el provider cumple, no que el consumer usa.

Ejercicio 3 — Un consumer nuevo sobre la misma costura. Reservo agrega una función monthly_report(repo, room_id) que usa repo.find_by_room(room_id) para contar cuántas reservas confirmadas tiene una sala. Es un nuevo consumer de la costura del repositorio. Escribe un test del consumer para ella (contra el fake) y di en qué cláusula del contrato se apoya —y en cuál no debería apoyarse—.

Ver solución

Un test del consumer posible, contra el fake:

def test_monthly_report_counts_confirmed_bookings():
    repo = FakeBookingRepository()
    repo.save(Booking(id="bk-1", room_id="focus", member_id="m-ana",
                      start=START, end=END, status="confirmed", price_cents=6000))
    repo.save(Booking(id="bk-2", room_id="focus", member_id="m-ivan",
                      start=START, end=END, status="cancelled", price_cents=0))

    count = monthly_report(repo, "focus")

    assert count == 1        # solo la confirmada

En qué cláusula se apoya: en la 4 del contrato —"find_by_room devuelve solo las reservas de esa sala"—. monthly_report confía en que lo que recibe son exactamente las reservas de focus (ninguna de otra sala colada, ninguna de focus faltante), y sobre ese conjunto filtra por estado. Esa es una promesa legítima del contrato, así que apoyarse en ella es correcto.

En qué no debería apoyarse: en ningún orden de la lista. El contrato no promete que find_by_room devuelva las reservas ordenadas (por fecha, por precio, por id). Si monthly_report hiciera algo como "toma la primera de la lista" o "asume que vienen por fecha", estaría suponiendo de más —el error que la lección 6 caza—. Como aquí solo cuenta (una operación que no depende del orden), el consumer está limpio: usa exactamente lo que la cláusula 4 promete, ni más ni menos. Contar es seguro; ordenar-por-posición no lo sería.

Resumen y siguiente paso

En esta lección te sentaste en la silla del consumer y aprendiste a mirar la costura desde BookingService: "yo mando X y espero recibir Y". Escribiste un test del consumer que corre contra el FakeBookingRepository —porque el fake honra el contrato y hace de stand-in fiel, sin base de datos— y viste que hace dos cosas a la vez: verifica que el consumer orquesta bien dado el contrato cumplido, y documenta en qué promesas se apoya (que save/get guardan y recuperan, que get lanza para un id ausente). Con el mesero y la cocina entendiste el filo de la lección: un consumer robusto se apoya solo en lo prometido, y un test del consumer bien hecho lo ejercita contra un provider que promete el contrato y nada más, para que cualquier suposición de más se note. Y viste que esto es, en pequeño y en proceso, la misma idea Pact del consumer test contra un stand-in.

Antes de avanzar deberías poder: escribir un test del consumer contra el fake; explicar por qué el fake y no SQLite es la elección correcta para ese test; distinguir un test del consumer de uno de integración; y nombrar la diferencia entre apoyarse en una cláusula del contrato y apoyarse en un regalo no prometido del provider actual.

Lo que sigue es cambiarse de silla. La lección 3 se sienta en la del provider: la pregunta espejo, "dado X, yo devuelvo Y", donde SqliteBookingRepository verifica que cumple cada cláusula del contrato, aislado del consumer. Verás la misma batería que aquí dimos por cumplida, ahora del lado que tiene que cumplirla —y por qué ese lado sí toca el SQLite real—.

Recursos

  • docs.pact.io — Consumer testing — la descripción oficial de qué es un test del consumer en el modelo consumer-driven: correr el consumer contra un stand-in que responde según el contrato y registra sus expectativas. El marco industrial de lo que esta lección hace a mano con el fake.
  • Documentación de pytest — Cómo escribir y correr tests — la referencia de las aserciones con assert y pytest.raises que usamos para verificar tanto el resultado (saved.status) como el error esperado (KeyError) del consumer.
  • test-doubles-and-test-data-guide — la guía hermana donde construiste el FakeBookingRepository que aquí hace de stand-in del provider; útil para recordar por qué un fake es una implementación que de verdad funciona, y por eso puede honrar un contrato.
  • Módulo 3 de esta guía — Contract testing: consumer y provider — donde se definió el contrato de las cuatro cláusulas en las que el consumer de esta lección se apoya.