Módulo 8: Proyecto: contrato + integración de Reservo

4. Verificar el contrato contra el `SqliteBookingRepository` real

Descripción

Aquí se cierra el segundo entregable. En la lección anterior corriste el contrato contra el fake y viste cuatro verdes que, aprendiste, medían coherencia contigo mismo, no coincidencia con lo real. Ahora traes la referencia externa: el SqliteBookingRepository, la pieza que de verdad corre en producción. Corres la batería completa —sin filtro, los ocho casos— y ves [fake] y [sqlite] uno al lado del otro, las cuatro cláusulas verdes en ambas implementaciones. Ese es el momento en que tu contrato deja de ser un autoexamen y se vuelve una certificación: el fake y el real, verificados contra el mismo spec, cumplen lo mismo.

Y hay un argumento lógico detrás de ese verde que conviene nombrar, porque es el corazón de por qué el contrato funciona: la transitividad. Si el fake cumple el contrato, y el real cumple el contrato, entonces el fake y el real coinciden en todo lo que el contrato cubre —no porque los hayas comparado directamente uno contra otro, sino porque ambos se midieron contra la misma vara—. No hay dos specs que puedan divergir: hay uno solo, corrido dos veces. Esa es la garantía técnica que convierte "espero que mi fake no mienta" en "mi fake no puede mentir sobre nada que el contrato cubra sin que un test rojo lo delate". En esta lección la ves funcionar, con salida real, y entiendes por qué "una batería, dos providers" es la forma —y no dos suites gemelas.

Conexión con el módulo: esta lección completa el entregable 2 y prepara el 3. Con el contrato verde por ambos lados, tienes la primera capa de la garantía: cada cláusula que enumeraste está verificada contra el fake y contra el real. Lo que el contrato no cubre —los bugs de uso que no enumeraste— es lo que la integración de punta a punta de la lección 5 caza. Y el contrato verde de hoy es también el que, en la lección 7, se pondrá rojo cuando alguien rompa una promesa: la red de seguridad que aquí queda armada. Hoy la ves en su estado sano; después la verás atrapar.

Analogía: dos relojes contra la hora oficial

Tienes dos relojes: el de tu cocina (barato, cómodo, siempre a la vista) y el de la estación de tren (el que de verdad manda cuándo sale tu tren). Quieres estar seguro de que puedes confiar en el de la cocina para no perder el tren. Una forma torpe sería comparar los dos relojes directamente cada mañana, cara a cara —imposible, están en sitios distintos—. La forma buena es que cada uno se ponga en hora contra la hora oficial: la señal de tiempo del observatorio nacional, la misma para los dos. Si el reloj de la cocina marca la hora oficial, y el de la estación marca la hora oficial, entonces los dos marcan lo mismo —sin haberlos comparado nunca entre sí—. La hora oficial es la vara común, y la coincidencia entre los relojes se sigue de que ambos la cumplen.

El contrato es la hora oficial. El FakeBookingRepository es el reloj de la cocina —el que usas todo el día en tus unit tests rápidos— y el SqliteBookingRepository es el reloj de la estación —el que manda en producción—. No comparas el fake contra el real directamente, objeto por objeto; los pones a los dos contra la misma batería de cláusulas. Cuando la lección 3 mostró [fake] en verde, el reloj de la cocina marcaba la hora oficial. Cuando esta lección muestra [sqlite] también en verde, el reloj de la estación también la marca. La conclusión es transitiva y sólida: marcan lo mismo, así que puedes confiar en el de la cocina para no perder el tren. Y si un día uno se desajustara —el fake que devuelve None, el real que no serializa—, dejaría de marcar la hora oficial, y su cláusula se pondría roja, señalándolo. La vara común es lo que hace posible confiar en el reloj cómodo.

Ejemplo trabajado: los ocho casos, fake y real, en verde

Corramos la batería completa, sin -k, para que pytest ejecute los ocho casos: cuatro cláusulas por dos providers. Este es el comando de la entrega —así se corre un contrato, con los dos lados juntos—:

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

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

tests/test_repository_contract.py::test_save_then_get_returns_the_same_booking[fake] PASSED [ 12%]
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[fake] PASSED [ 37%]
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[fake] PASSED [ 62%]
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[fake] PASSED [ 87%]
tests/test_repository_contract.py::test_find_by_room_returns_only_that_rooms_bookings[sqlite] PASSED [100%]

============================== 8 passed in 0.03s ==============================

Ocho verdes. Léelos como un reporte de inspección: cada cláusula pasó tanto [fake] como [sqlite]. Esa es la certificación completa del contrato —el segundo entregable, terminado—: el fake y el real se comportan igual en las cuatro cláusulas, verificado, no supuesto. Y fíjate en la economía que ya viste en el módulo 3: no escribiste ocho tests, escribiste cuatro; la fixture parametrizada duplicó cada uno. Compara este resultado con el 4 passed, 4 deselected de la lección 3. Ahí tenías medio contrato —el reloj de la cocina contra la hora oficial—; aquí tienes el contrato entero —los dos relojes contra la misma vara—. La diferencia entre los dos comandos es un -k fake de menos, y es toda la diferencia entre "coherente conmigo" y "coincide con lo real".

Detente un segundo en la cláusula 1, test_save_then_get_returns_the_same_booking, porque su [sqlite] verde es el que más dice. Esa cláusula compara la reserva recuperada con la guardada, campo por campo, incluido el start. Que pase contra [sqlite] significa que el SqliteBookingRepository.get reconstruye el datetime al leer —convierte el texto ISO de vuelta con datetime.fromisoformat—, porque si devolviera el start como str, un Booking con start de texto no sería igual a uno con start de datetime, y el caso estaría en rojo. El verde de [sqlite] en esa cláusula es la prueba de que el bug del datetime que la guía persiguió desde el módulo 1 está cerrado en este provider. El contrato no solo certifica: documenta que la costura está sana.

Por qué la transitividad, y no la comparación directa

Vale la pena entender por qué el contrato usa una vara común en vez de comparar el fake contra el real directamente. Podrías imaginar un test que hiciera algo como "guarda lo mismo en el fake y en el real, y compara que get devuelve objetos iguales en ambos". Suena razonable, y para casos simples funcionaría. Pero tiene dos problemas que el contrato-como-vara evita.

Primero, la comparación directa no dice qué comportamiento es el correcto. Si el fake y el real difieren, ¿cuál está bien? La comparación directa solo dice "difieren"; no tiene una noción de lo que debería pasar. El contrato sí: cada cláusula afirma el comportamiento que el consumer necesita, así que cuando un lado falla, sabes que ese lado se desvió del comportamiento correcto, no solo que los dos no coinciden. La vara no es cualquiera: es la que dictó el consumer.

Segundo, la comparación directa acopla las dos implementaciones. Un test que guarda en ambos y compara tiene que conocer los dos providers a la vez y construirlos juntos; añadir un tercero (un repositorio sobre archivo) obliga a reescribir la comparación. Con la vara común, cada provider se mide por separado contra el mismo spec: añadir uno es una palabra en params, y su coincidencia con los otros se sigue por transitividad, sin escribir una sola comparación nueva. La vara escala; la comparación cara a cara, no.

Por eso "una batería, dos providers" —una, no dos suites gemelas, y no una comparación directa— es la forma canónica. Hay un spec. Si una cláusula cambia, cambia para ambos a la vez, porque es la misma función. Si un provider deja de cumplirla, su corrida se pone roja y el id entre corchetes lo nombra, mientras el otro sigue verde. La única manera de que el fake "pase por bueno" es que de verdad cumpla las cuatro cláusulas, igual que el real —y eso es exactamente lo que querías garantizar—.

Lo que el contrato verde garantiza, y lo que no

Con el entregable 2 completo, conviene ser preciso sobre qué compraste con estos ocho verdes, porque marca la frontera con el entregable 3.

Lo que garantiza: que el fake y el real coinciden en las cuatro cláusulas que enumeraste. Cualquier divergencia en un comportamiento que escribiste como cláusula saltará en rojo. Si mañana el real deja de lanzar en un id ausente, o deja de reconstruir el datetime, o empieza a duplicar en vez de actualizar, el contrato lo caza. Eso es mucho: cubre las divergencias conocidas, las que la experiencia te enseñó a temer.

Lo que no garantiza: que el fake y el real coincidan en algo que no escribiste como cláusula. El contrato tiene exactamente los huecos que le dejes. Si olvidaste una cláusula sobre cómo se comporta find_by_room con reservas canceladas, esa diferencia vive en el hueco, invisible al contrato verde. Y —más importante para lo que viene— el contrato inspecciona la forma de los datos campo por campo; no ejercita el uso que BookingService hace de ellos en un flujo vivo. Un bug que solo aparece cuando cancel usa el start en una resta puede esconderse en un contrato que verificó el start con == pero que, si tuviera un hueco justo ahí, no lo vería.

Esa segunda limitación es la razón de ser del entregable 3. El contrato certifica cada pieza contra el spec, cláusula por cláusula; la integración de punta a punta verifica que las piezas, usadas de verdad en un flujo, colaboran —y caza los bugs de uso que ninguna cláusula enumeró—. Con el contrato verde por ambos lados, tienes la primera capa. La lección 5 añade la segunda.

Errores comunes

Correr el contrato con -k y creer que es la entrega. Qué pasa: por costumbre de la lección 3, alguien corre siempre con -k fake o -k sqlite y entrega un solo lado. Por qué pasa: el filtro se quedó del paso anterior. Cómo detectarlo: si tu salida dice deselected, filtraste; la entrega del contrato no filtra. Cómo corregirlo: la batería del contrato se corre entera, sin -k, para ver [fake] y [sqlite] juntos. El 4 passed de un lado no es el contrato; el 8 passed de los dos, sí.

Confundir "los dos verdes" con "los dos idénticos en todo". Qué pasa: al ver los ocho verdes, alguien concluye que el fake y el real son intercambiables para cualquier propósito. Por qué pasa: "cumplen el mismo contrato" se siente como "son iguales". Cómo detectarlo: el fake y el real coinciden en lo que el contrato cubre, no necesariamente en lo demás —velocidad, persistencia en disco, comportamiento fuera de las cláusulas—. Cómo corregirlo: recuerda que el contrato garantiza coincidencia en sus cláusulas, ni más ni menos. Para lo que el contrato no cubre, sigue habiendo diferencias reales (el real persiste, el fake no; el real es más lento), y por eso el entregable 3 prueba el flujo real y no se conforma con el contrato.

Sospechar del contrato cuando ambos lados fallan. Qué pasa: un día una cláusula falla en [fake] y [sqlite] a la vez, y alguien busca el bug en los dos providers. Por qué pasa: dos rojos parecen dos bugs. Cómo detectarlo: es raro que dos implementaciones independientes se rompan igual a la vez; cuando ambos lados de la misma cláusula fallan, lo más probable es que el problema esté en el test o en el contrato mismo —una aserción mal escrita, un dato de ejemplo equivocado—, no en los providers. Cómo corregirlo: cuando falla un solo lado, sospecha de ese provider (se desvió del contrato); cuando fallan los dos, sospecha de la cláusula. La asimetría del rojo es diagnóstico; léela antes de tocar código.

Ejercicios

Ejercicio 1 — El argumento de transitividad, escrito. Enuncia, en forma de silogismo, por qué el contrato verde por ambos lados garantiza que el fake y el real coinciden, sin haberlos comparado directamente. Luego di qué pasaría con el argumento si el contrato tuviera solo tres cláusulas en vez de cuatro.

Ver solución

El silogismo:

  1. El FakeBookingRepository cumple las cuatro cláusulas del contrato (lo mostró [fake] en verde).
  2. El SqliteBookingRepository cumple las cuatro cláusulas del contrato (lo mostró [sqlite] en verde).
  3. Por tanto, el fake y el real coinciden en el comportamiento que las cuatro cláusulas describen —sin haberlos comparado uno contra otro, porque ambos se midieron contra la misma vara—.

Con solo tres cláusulas: el argumento seguiría siendo válido, pero garantizaría menos. La conclusión sería "el fake y el real coinciden en el comportamiento que las tres cláusulas describen". El comportamiento que la cuarta cláusula cubría —digamos, find_by_room devuelve solo las reservas de esa sala— quedaría fuera de la garantía: el fake y el real podrían divergir ahí sin que ningún rojo lo delatara. La transitividad solo cubre lo que la vara mide. Por eso el alcance de la garantía es exactamente el conjunto de cláusulas: cada cláusula que quitas es una puerta que dejas abierta, y cada una que añades (dirigida por una necesidad real) es una que cierras.

Ejercicio 2 — Qué cláusula prueba que el datetime está arreglado. De las cuatro cláusulas, ¿cuál falla contra [sqlite] si el SqliteBookingRepository.get devolviera el start como str (el bug del módulo 1, sin el arreglo fromisoformat)? Explica por qué, y por qué esa misma cláusula pasa contra [fake].

Ver solución

Falla test_save_then_get_returns_the_same_booking[sqlite], la cláusula 1. Esa cláusula hace repo.save(booking) y luego assert repo.get("bk-1") == booking, comparando el objeto entero. La comparación de dataclasses compara campo por campo, incluido el start. Si get devolviera el start como str'2026-03-10T09:00:00'— mientras el booking guardado lo tiene como datetime(2026, 3, 10, 9), los dos objetos no serían iguales (str != datetime en ese campo), y la aserción fallaría con [sqlite].

Por qué pasa contra [fake]: el FakeBookingRepository guarda el objeto entero en un dict y lo devuelve idéntico —nunca serializa nada—, así que el start vuelve como el datetime original y la comparación es datetime == datetime, verdadera. La cláusula pasa contra el fake y fallaría contra el real buggy: exactamente la asimetría que el contrato existe para exhibir. Que en la entrega real ambos lados estén verdes es la prueba de que el SqliteBookingRepository sí reconstruye el datetime (con fromisoformat), cerrando el bug.

Ejercicio 3 — Comparación directa contra vara común. Un compañero propone reemplazar el contrato por un solo test que guarde una reserva en el fake y en el real y compare que ambos get devuelven objetos iguales. Da dos razones concretas por las que la vara común (el contrato) es mejor, usando el caso de añadir un tercer provider (un repositorio sobre archivo).

Ver solución

Razón 1 — la vara común dice qué es correcto; la comparación directa no. Si el test de comparación directa encontrara que el fake y el real difieren, solo diría "difieren"; no sabría cuál está bien. El contrato afirma el comportamiento que el consumer necesita, así que cuando un lado falla, sabes que ese lado se desvió de lo correcto. Al añadir el repositorio sobre archivo, el contrato lo mide contra el mismo estándar del consumer; la comparación directa tendría que decidir arbitrariamente contra cuál de los dos existentes compararlo.

Razón 2 — la vara común escala; la comparación directa acopla. Añadir el repositorio sobre archivo al contrato es agregar "file" a params: una palabra, y las cuatro cláusulas lo certifican, con su coincidencia con los otros dos siguiéndose por transitividad. Con la comparación directa tendrías que escribir nuevas comparaciones —fake contra archivo, real contra archivo—, porque comparar de a pares no es transitivo en el código: cada nuevo provider multiplica los pares que hay que escribir. La vara común convierte "N providers coinciden" en "cada uno cumple el contrato", que es lineal; la comparación directa es cuadrática en pares.

En una frase: la vara común tiene una noción de lo correcto (el consumer) y mide a cada provider por separado; la comparación directa carece de esa noción y acopla los providers entre sí. Por eso el contrato es la forma canónica.

Resumen y siguiente paso

En esta lección cerraste el segundo entregable: corriste la batería completa —sin filtro— y viste los ocho casos en verde, [fake] y [sqlite] para cada una de las cuatro cláusulas. Ese verde por ambos lados convierte tu contrato de un autoexamen en una certificación: el fake y el real, medidos contra el mismo spec, cumplen lo mismo. Con los dos relojes contra la hora oficial entendiste la transitividad —si ambos marcan la hora oficial, marcan lo mismo, sin haberlos comparado entre sí— y por qué la vara común es mejor que la comparación directa: tiene una noción de lo correcto (el consumer) y escala a más providers con una palabra. Y precisaste el alcance: el contrato garantiza coincidencia en las cláusulas que enumeraste, no en lo que no escribiste ni en el uso vivo de los datos.

Antes de avanzar deberías poder: correr el contrato entero y leer los ocho casos; enunciar el argumento de transitividad y su alcance; y explicar por qué "una batería, dos providers" supera a la comparación directa entre implementaciones.

Con el contrato verde tienes la primera capa de la garantía: las divergencias que enumeraste están cubiertas. Falta la segunda, la que cubre lo que el contrato no ve —los bugs de uso que solo aparecen cuando las piezas trabajan juntas en un flujo real—. En la lección 5 construyes el tercer entregable: la prueba de integración de punta a punta, BookingService + SqliteBookingRepository real, ejercitando bookgetcancel de verdad, cruzando la costura como lo hará en producción.

Recursos