Módulo 4: Verificar el contrato desde ambos lados
3. El lado del provider
Descripción
Cámbiate de silla. En la lección 2 miraste la costura desde BookingService, el consumer que usa el repositorio y pregunta "¿me apoyo solo en lo prometido?". Ahora te sientas del otro lado, en el provider: el componente que implementa el colaborador. En la costura del repositorio, el provider es el SqliteBookingRepository real (y también el fake, que es otro provider de la misma costura). Su punto de vista es la pregunta espejo del consumer: "dado X, yo devuelvo Y". Dado que me piden guardar una reserva y luego leerla, yo devuelvo esa misma reserva, con los tipos correctos. Dado que me piden un id que no existe, yo lanzo. Dado que me guardan dos veces el mismo id, yo actualizo sin duplicar.
El test del provider verifica que la implementación cumple cada cláusula del contrato, aislada del consumer. No hay BookingService a la vista: no hace falta un servicio, ni un calendario, ni un pago, para preguntarle al repositorio si honra su contrato. Le hablas directo —repo.save(...), repo.get(...)— y compruebas que responde según lo pactado. Y aquí sí toca la pieza real: mientras el consumer se probaba contra el fake (rápido, en memoria), el provider real es exactamente lo que hay que verificar, porque es quien tiene que serializar el datetime, escribir en la tabla, hacer cumplir el PRIMARY KEY. El test del provider es donde SQLite rinde cuentas.
Conexión con el módulo: esta lección cierra el par que la 1 anunció. Con la 2 (consumer) y la 3 (provider) tienes las dos sillas. La lección 4 mostrará que estos dos lados no son dos baterías distintas, sino la misma batería corrida contra los dos providers, y por qué correrla contra ambos es la garantía de que ninguno miente. Aquí verás el test del provider como lo que es: la batería de contrato del módulo 3, mirada desde el lado que tiene que pasarla, y filtrada al provider real para verlo rendir cuentas solo.
Analogía: la inspección de fábrica contra la norma
Vuelve al enchufe y el tomacorriente de la lección 1, pero entra ahora a la fábrica de tomacorrientes. Ahí no hay lámparas: hay un inspector con un medidor y una copia de la norma eléctrica. Su trabajo es tomar un tomacorriente recién salido de la línea y verificarlo, punto por punto, contra la norma: "¿entrega 120 voltios? sí. ¿las clavijas están a la distancia estándar? sí. ¿la tierra está donde la norma la exige? sí". No necesita conectar ninguna lámpara para hacer su trabajo; le basta el producto y la norma. Si el tomacorriente cumple cada cláusula, sale aprobado; si entrega 240 voltios, se rechaza ahí mismo, en la fábrica, antes de que llegue a una pared.
Ese inspector es el test del provider. La norma es el contrato. El tomacorriente es el SqliteBookingRepository. El inspector le hace las preguntas del contrato directamente —guarda y devuelve, id ausente, doble guardado, filtro por sala— y verifica que cada respuesta cumple. No arranca BookingService (no conecta una lámpara) porque no lo necesita: la pregunta "¿este provider cumple la norma?" se responde con el provider y la norma, nada más. Y fíjate en el contraste con la lección 2: el fabricante de la lámpara probaba la lámpara contra la norma (el consumer test); el fabricante del tomacorriente prueba el tomacorriente contra la misma norma (el provider test). Dos inspecciones, dos productos, una sola norma —y ninguna de las dos necesita el producto de la otra—.
Ejemplo trabajado: el provider real rindiendo cuentas
El test del provider es la batería de contrato del módulo 3, mirada desde el lado del SqliteBookingRepository. Como la batería está parametrizada con params=["fake", "sqlite"], el "test del provider real" es simplemente esa batería filtrada al provider sqlite: las mismas cuatro cláusulas, corridas solo contra la implementación real, para verla rendir cuentas sola. Recordemos la batería (es la misma de la lección 1):
# tests/test_repository_contract.py — las cuatro clausulas, contra cada provider
def test_save_then_get_returns_the_same_booking(repo):
repo.save(a_booking())
got = repo.get("bk-1")
assert got.id == "bk-1"
assert got.room_id == "focus"
assert got.start == START # datetime, no str <-- SQLite tiene que serializar y reconstruir
assert got.price_cents == 6000
assert got.status == "confirmed"
def test_get_of_a_missing_id_raises(repo):
with pytest.raises(KeyError):
repo.get("does-not-exist") # <-- SQLite tiene que lanzar, no devolver None
def test_saving_the_same_id_twice_updates_not_duplicates(repo):
repo.save(a_booking(status="confirmed"))
repo.save(a_booking(status="cancelled"))
got = repo.get("bk-1")
assert got.status == "cancelled" # <-- el ON CONFLICT ... DO UPDATE de SQLite
assert len(repo.find_by_room("focus")) == 1
def test_find_by_room_returns_only_that_rooms_bookings(repo):
repo.save(a_booking(id="bk-1", room_id="focus"))
repo.save(a_booking(id="bk-2", room_id="studio"))
ids = {b.id for b in repo.find_by_room("focus")}
assert ids == {"bk-1"} # <-- el WHERE room_id = ? de SQLite
Ahora la corremos filtrada al provider real, con el selector -k sqlite de pytest, que solo ejecuta los tests cuyo id contiene sqlite:
Qué esperar. En mi máquina (Python 3.14.0, pytest 9.1.1):
python3 -m pytest tests/test_repository_contract.py -v -k sqlite
============================= test session starts ==============================
platform darwin -- Python 3.14.0, pytest-9.1.1, pluggy-1.6.0
collected 8 items / 4 deselected / 4 selected
tests/test_repository_contract.py::test_save_then_get_returns_the_same_booking[sqlite] PASSED [ 25%]
tests/test_repository_contract.py::test_get_of_a_missing_id_raises[sqlite] PASSED [ 50%]
tests/test_repository_contract.py::test_saving_the_same_id_twice_updates_not_duplicates[sqlite] PASSED [ 75%]
tests/test_repository_contract.py::test_find_by_room_returns_only_that_rooms_bookings[sqlite] PASSED [100%]
======================= 4 passed, 4 deselected in 0.01s ========================
Cuatro verdes, y las otras cuatro (las [fake]) deselected —pytest las recogió pero no las corrió, porque su id no contiene sqlite—. Esto es el provider rindiendo cuentas solo: el SqliteBookingRepository real cumple las cuatro cláusulas del contrato, sin BookingService de por medio. Cada verde es una promesa cumplida por la implementación real: test_save_then_get_returns_the_same_booking[sqlite] verifica que SQLite serializa el datetime a texto en save y lo reconstruye a datetime en get (el got.start == START pasa); test_get_of_a_missing_id_raises[sqlite] verifica que un SELECT sin filas lanza KeyError en vez de devolver None; test_saving_the_same_id_twice_updates_not_duplicates[sqlite] verifica que el ON CONFLICT(id) DO UPDATE actualiza en vez de duplicar; y test_find_by_room_returns_only_that_rooms_bookings[sqlite] verifica que el WHERE room_id = ? no cuela reservas de otra sala. El provider real, aprobado por la norma.
La simetría —y la asimetría— entre los dos tests
Consumer y provider verifican el mismo contrato, pero no son imágenes idénticas. Conviene ver dónde son simétricos y dónde no, porque ahí está la razón de que hagan falta los dos.
Son simétricos en la norma. Ambos tests hablan del mismo contrato: las mismas cuatro cláusulas. El consumer se apoya en ellas ("espero que get lance"); el provider las cumple ("yo lanzo"). Es literalmente el mismo acuerdo visto desde los dos lados. Por eso, cuando en la lección 4 corras la batería contra ambos providers, no estarás escribiendo dos contratos: estás corriendo un contrato contra dos implementaciones.
Son asimétricos en el setup. El test del consumer necesita el consumer completo —BookingService con todos sus colaboradores doblados— y un provider cualquiera que honre el contrato (el fake, por velocidad). El test del provider no necesita el consumer en absoluto: le habla directo a la implementación. Uno arma un servicio; el otro arma un repositorio. El setup de cada lado es el mínimo que ese lado necesita.
Son asimétricos en qué pieza es real. En el test del consumer, el provider está doblado (el fake): lo real que se prueba es la lógica del consumer. En el test del provider, la implementación es la real (SQLite): lo que se prueba es la fidelidad del provider. Cada test pone bajo el foco a un lado y da por bueno al otro. Por eso ninguno reemplaza al otro: un consumer perfecto no garantiza un provider fiel, y un provider fiel no garantiza que el consumer lo use bien. Solo los dos juntos cubren el contrato entero.
Esta asimetría explica por qué el consumer se prueba contra el fake y el provider contra SQLite, sin que sea contradictorio. No es que "unos tests usan el fake y otros el real" al azar: es que cada lado dobla lo que no está probando y deja real lo que sí. El consumer test dobla el provider (para aislar la lógica del consumer); el provider test no tiene nada que doblar, porque el provider es justo lo que examina.
Por qué el provider se prueba contra lo real (y el consumer no)
Detengámonos en la pregunta que más confunde: si en la lección 2 insistimos en probar el consumer contra el fake "porque el fake honra el contrato", ¿por qué ahora el provider se prueba contra SQLite y no contra el fake? La respuesta es directa: el test del provider existe precisamente para verificar que la implementación real honra el contrato. Probar el provider contra el fake sería absurdo —verificarías que el fake cumple el contrato usando el fake, un círculo—. El punto del test del provider es poner a prueba a la implementación bajo sospecha: la que serializa, la que escribe en disco, la que puede divergir. Esa es SQLite.
Dicho de otro modo: el fake se da por bueno porque el contrato lo mantiene honesto, y quien lo mantiene honesto es esta misma batería corrida contra él. El provider real no se da por bueno; se verifica. En la lección 4 verás que la batería corre contra los dos —el fake para confirmar que sigue honrando el contrato, el real para confirmar que lo cumple—, y que ese "los dos" es lo que garantiza que el fake que usaste en el consumer test no estaba mintiendo. Por ahora, la regla: el test del provider apunta al provider que puede fallar, y ese es el real.
Errores comunes
Meter BookingService en el test del provider. Qué pasa: alguien prueba que SQLite cumple el contrato pasando por BookingService.book, en vez de llamar repo.save/repo.get directo. Por qué pasa: se arrastra el hábito de probar "el flujo completo". Cómo detectarlo: si tu test del provider construye un Calendar, un StubPaymentGateway y un BookingService, estás probando dos cosas a la vez y ya no aíslas al provider. Cómo corregirlo: háblale al repositorio directo. El test del provider pregunta "¿este repositorio cumple el contrato?", y eso se responde con el repositorio y el contrato, sin servicio. Si book tuviera un bug, no querrías que contaminara el veredicto sobre SQLite.
Probar el provider contra el fake. Qué pasa: alguien corre la batería solo con -k fake y concluye "el provider cumple el contrato". Por qué pasa: se pierde de vista cuál provider está bajo prueba. Cómo detectarlo: si tu "test del provider" nunca toca SqliteBookingRepository, no verificaste la implementación real —verificaste el fake, que ya dabas por honesto—. Cómo corregirlo: el test del provider real corre contra sqlite (-k sqlite, o la batería completa que incluye ambos). El fake es el stand-in del consumer test; el objeto del provider test es la implementación que puede divergir.
Creer que un provider aprobado hoy queda aprobado para siempre. Qué pasa: los cuatro verdes del [sqlite] dan tranquilidad y se deja de correr la batería cuando alguien toca SqliteBookingRepository. Por qué pasa: el verde se siente definitivo. Cómo detectarlo: si el contrato no vuelve a correr tras cada cambio del provider, un breaking change (lección 5) se cuela sin ser visto. Cómo corregirlo: el test del provider vale por volver a correr en cada cambio de la implementación. Un provider "aprobado" que cambió y no se re-verificó es un provider sin aprobar. La inspección de fábrica se hace en cada lote, no una vez.
Ejercicios
Ejercicio 1 — Traduce cada cláusula a lo que SQLite debe hacer. Para cada una de las cuatro cláusulas del contrato, escribe la frase "para pasar esta cláusula, SqliteBookingRepository tiene que...", nombrando el mecanismo concreto de SQLite que la cumple.
Ver solución
- Guardar-y-leer devuelve la misma reserva: ...tiene que serializar los campos en
save(eldatetimea texto ISO con.isoformat(), el resto directo) y reconstruirlos enget(el texto de vuelta adatetimecondatetime.fromisoformat, los enteros y textos tal cual), de modo que la reserva que sale sea igual a la que entró, campo por campo. getde un id ausente lanza: ...tiene que detectar que elSELECT ... WHERE id = ?no devolvió filas (fetchone()daNone) y lanzarKeyErroren ese caso, en vez de devolver elNonecrudo.- Guardar dos veces el mismo id actualiza sin duplicar: ...tiene que usar
INSERT ... ON CONFLICT(id) DO UPDATE SET ..., apoyado en que la columnaidesPRIMARY KEY, para que el segundosavedel mismo id sobrescriba la fila en vez de crear una segunda. find_by_roomdevuelve solo las reservas de esa sala: ...tiene que filtrar conSELECT ... WHERE room_id = ?, de modo que una reserva de otra sala nunca aparezca en el resultado.
Lo esencial: cada cláusula del contrato se traduce en una responsabilidad concreta de la implementación. El test del provider es el que verifica que esas responsabilidades se cumplen. Un provider distinto (por ejemplo, uno sobre PostgreSQL) tendría otros mecanismos —otra sintaxis de upsert— pero debería cumplir las mismas cláusulas. El contrato es estable; la implementación varía.
Ejercicio 2 — El selector -k. En el ejemplo trabajado corrimos -k sqlite y pytest reportó "4 deselected". Explica qué hizo el selector, y escribe el comando para correr, al revés, solo el lado del fake. ¿Qué probaría esa corrida y qué no?
Ver solución
El selector -k sqlite le dice a pytest: "de todos los tests recogidos, corre solo aquellos cuyo id contenga la subcadena sqlite". Como la batería está parametrizada, cada test tiene dos variantes —...[fake] y ...[sqlite]—; -k sqlite selecciona las cuatro [sqlite] y deselecciona las cuatro [fake] (las recoge pero no las ejecuta, de ahí el "4 deselected"). Es una forma de mirar un solo lado del contrato sin borrar el otro.
El comando para el lado del fake:
python3 -m pytest tests/test_repository_contract.py -v -k fake
Esa corrida probaría que el FakeBookingRepository sigue cumpliendo las cuatro cláusulas del contrato —útil para confirmar que el stand-in del consumer test no se desincronizó—. Lo que no probaría es la implementación real: no dice nada sobre si SqliteBookingRepository serializa bien, lanza en el id ausente o filtra por sala. Por eso ninguno de los dos lados basta solo: -k fake verifica el doble, -k sqlite verifica el real, y solo correr la batería completa (ambos) da el contrato entero. El -k es para inspeccionar un lado, no para reemplazar la corrida completa.
Ejercicio 3 — Un segundo provider. Imagina que Reservo añade InMemorySqliteViaFile, otro provider real que guarda en un archivo SQLite en disco en vez de :memory:. ¿Qué tendrías que cambiar en la batería de contrato para verificarlo como tercer provider, y qué no cambiaría? ¿Qué te diría si pasa las cuatro cláusulas?
Ver solución
Lo que cambia: solo la fixture que fabrica el provider. Añades un tercer valor a params y su rama de construcción:
@pytest.fixture(params=["fake", "sqlite", "sqlite-file"])
def repo(request):
if request.param == "fake":
return FakeBookingRepository()
if request.param == "sqlite":
return SqliteBookingRepository(sqlite3.connect(":memory:"))
return SqliteBookingRepository(sqlite3.connect(tmp_path / "reservo.db"))
Lo que NO cambia: las cuatro cláusulas. Ni una línea de los cuatro test_... se toca. Ese es el punto entero del contrato: es un spec de comportamiento independiente de la implementación, así que verificar un provider nuevo es enchufarlo a la fixture, no reescribir las verificaciones. Cada test pasaría ahora a correr tres veces: [fake], [sqlite], [sqlite-file].
Qué te diría si pasa las cuatro cláusulas: que ese tercer provider honra el mismo contrato que los otros dos, y por tanto es intercambiable con ellos desde el punto de vista del consumer. BookingService podría usar cualquiera de los tres sin cambiar una línea, porque los tres cumplen las mismas promesas. Esa intercambiabilidad garantizada —"cualquier provider que pase el contrato sirve"— es el valor de tener un contrato en vez de atarse a una implementación. Y si el provider en disco fallara, digamos, la cláusula del doble guardado, sabrías al instante que su upsert no está bien, sin tocar nada del consumer.
Resumen y siguiente paso
En esta lección te sentaste en la silla del provider y aprendiste su pregunta espejo: "dado X, yo devuelvo Y". El test del provider verifica que la implementación cumple cada cláusula del contrato, aislada del consumer —sin BookingService, hablándole directo al repositorio—. Lo viste como lo que es: la batería de contrato del módulo 3, filtrada al provider real con -k sqlite, mostrando al SqliteBookingRepository rendir cuentas solo: los cuatro verdes que confirman que serializa y reconstruye el datetime, lanza en el id ausente, actualiza sin duplicar y filtra por sala. Con la inspección de fábrica contra la norma entendiste que este lado no necesita el producto del otro: al provider se le verifica con el provider y la norma. Y entendiste la simetría (la misma norma) y la asimetría (distinto setup, distinta pieza real) que hacen falta a los dos lados.
Antes de avanzar deberías poder: escribir un test del provider que le hable directo al repositorio; explicar por qué el provider se prueba contra lo real mientras el consumer se probaba contra el fake, sin que sea contradictorio; y traducir cada cláusula del contrato al mecanismo de SQLite que la cumple.
Lo que sigue une las dos sillas. La lección 4 muestra que el test del consumer y el del provider no viven en baterías separadas: son la misma batería corrida contra los dos providers a la vez, y ese "a la vez" es toda la garantía. Verás por qué correr un contrato contra ambos —no contra uno— es exactamente lo que cierra la brecha del módulo 1: la que dejaba a un fake mentir sin que nadie lo notara.
Recursos
- Documentación de pytest — Seleccionar tests con
-k— la referencia del selector que usamos para correr solo el lado del provider (-k sqlite) y leer el "4 deselected"; útil para inspeccionar un lado del contrato sin borrar el otro. - docs.pact.io — Provider verification — la descripción oficial de la verificación del provider en el modelo consumer-driven: tomar el contrato y comprobar que la implementación real lo cumple. La versión en red del test del provider que aquí corres contra SQLite.
sqlite3— Ejecutar sentencias yON CONFLICT(documentación de Python) — la referencia de los mecanismos de SQLite (elexecute, el upsert, elfetchone) que cada cláusula del contrato obliga al provider real a implementar.- Módulo 3 de esta guía — Contract testing: consumer y provider — donde se escribió la batería parametrizada que aquí filtramos al provider real; el origen de las cuatro cláusulas.