Módulo 5: Integración de verdad: componentes reales juntos
8. Mini-proyecto: una integración `book`→`get` contra SQLite
Descripción
Este es el cierre práctico del módulo. Las siete lecciones anteriores te dieron el concepto pieza por pieza —qué es una integración de verdad, cómo armar la costura BookingService↔SQLite, qué doblar y qué dejar real, el vocabulario solitario/sociable, cómo la integración caza lo que el unit no, y su costo—. Ahora te toca tejerlo todo en un solo entregable que demuestra que sabes escribir una prueba de integración real: tomas el book de Reservo, lo conectas a un SqliteBookingRepository de verdad, y verificas el flujo completo book→get contra una base de datos real, en verde. No es una herramienta nueva; es el módulo entero condensado en una suite de integración bien montada y su justificación.
Lo que de verdad se evalúa aquí no es que consigas el verde —eso, con lo que aprendiste, es directo— sino que puedas justificar cada decisión de diseño. Cualquiera puede enchufar un repositorio real y ver un PASSED. El entregable que importa es el razonamiento: nombrar cuál es la costura que integras, decir qué dejaste real y por qué (la costura bajo prueba), qué doblaste y por qué (lo lento, lo no determinista, lo externo), y explicar qué afirma tu verde que un unit test no podría afirmar. Un alumno que entrega la suite verde sin la justificación no demostró que entendió el módulo; uno que entrega la justificación completa demostró que ya sabe montar una integración con intención, no por imitación.
Conexión con el módulo: esta lección es el examen práctico del módulo 5 y su cierre. Recoge la definición de la lección 2, la costura de la 3, la regla de qué doblar de la 4, el vocabulario de la 5 y la conciencia del beneficio y el costo de las lecciones 6 y 7, y los presenta como un proyecto con entregables y solución de referencia. Después del enunciado, resume el módulo y te apunta al módulo 6, donde llevarás esta misma integración a las fronteras difíciles —transacciones, archivos, HTTP— con el manejo experto que aquí dejamos pendiente.
Analogía: la prueba de manejo en carretera real
Piensa en la diferencia entre el simulador de manejo y la prueba final en carretera real. En las clases usaste el simulador —dobles, todo controlado— para practicar cada maniobra sin riesgo. Pero la licencia no te la dan por el simulador: te la dan cuando manejas un carro de verdad, en una calle de verdad, con el examinador al lado, y demuestras que las maniobras que practicaste funcionan en el mundo real. No es una maniobra nueva; es las mismas que ya sabes, ahora sobre el asfalto de verdad. Y el examinador no solo mira que llegues: te pregunta por qué frenaste ahí, por qué cediste el paso, por qué elegiste ese carril. Manejar bien no basta; hay que manejar bien con criterio y poder explicarlo.
Tu mini-proyecto es esa prueba en carretera. Ya practicaste la integración en las lecciones anteriores —el simulador—; ahora la entregas como un trabajo terminado: BookingService real sobre SqliteBookingRepository real, el book→get sobre el asfalto de verdad (la base de datos), en verde. Y como el examinador, lo que evalúa el proyecto no es solo el verde, sino que sepas justificar cada decisión: por qué el repositorio va real, por qué el pago y el correo van doblados, qué te garantiza el verde. Aprobar es demostrar que ya no sigues una receta: montas una integración con criterio propio.
El proyecto: enunciado formal
Tu tarea es escribir y entregar una prueba de integración de book→get de Reservo contra un SqliteBookingRepository real, verificando el flujo completo, y justificar su diseño. Concretamente:
Escribe una suite de integración que reserve Focus 3 h para la socia pro Ana con BookingService real sobre un SqliteBookingRepository real, y verifique que la reserva quedó correctamente persistida y es recuperable desde la base de datos de verdad.
- La costura bajo prueba es
BookingService↔BookingRepository. El repositorio va real (SqliteBookingRepository); no se dobla, porque es la junta que pruebas. - Los demás colaboradores —pago, correo, reloj— van doblados (
StubPaymentGateway,SpyEmailSender,FixedClock), por la regla de la lección 4: lo externo, lo no determinista, lo que cuesta dinero. - Verifica el flujo completo: que
getrecupera la reserva con suid, suroom_id, sustatusconfirmado y suprice_centsde6000. Y, como segunda cara del flujo, quefind_by_roomla encuentra en la base de datos real.
Entregables
- La suite de integración, en verde con pytest:
book→getcontra elSqliteBookingRepositoryreal, verificando los campos que cruzan la costura, más un test defind_by_room. Cada aserción mira la base de datos real a través del repositorio. - El mapa de decisiones de doblado. Para cada uno de los colaboradores —
repo,payments,emails,clock,calendar— escribe si lo dejaste real o lo doblaste y una frase de justificación con la regla de la lección 4. Este es el entregable que más pesa. - La afirmación del verde. Escribe qué garantiza exactamente tu suite en verde, y qué no garantiza —qué queda fuera (por ejemplo, la persistencia física entre reinicios, o el
startcomodatetime, que son otros tests)—. - La salida real de pytest. Pega el reporte que prueba que la suite corre verde en tu máquina —tu propio bloque "Qué esperar"—.
Ejemplo trabajado: la suite de referencia en verde
Aquí está la suite de referencia completa, la que tú entregarías. Dos tests: el book→get que verifica el flujo básico, y el find_by_room que verifica la segunda cara de la costura. Léela entera; luego la corremos y desglosamos el razonamiento en la solución de referencia.
# tests/test_reservo_integration.py — mini-proyecto: book -> get contra SQLite real
import sqlite3
from datetime import datetime
from reservo.calendar import Calendar
from reservo.doubles import FixedClock, SpyEmailSender, StubPaymentGateway
from reservo.models import Member, Room
from reservo.services import BookingService
from reservo.sqlite_repo import SqliteBookingRepository
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)
def test_book_persists_a_confirmed_booking_in_real_sqlite():
repo = SqliteBookingRepository(sqlite3.connect(":memory:"))
service = make_service(repo)
booking = service.book(FOCUS, ANA, START, END)
saved = repo.get(booking.id) # leido de la tabla real
assert saved.id == booking.id
assert saved.room_id == "focus"
assert saved.status == "confirmed"
assert saved.price_cents == 6000
def test_the_booking_is_findable_by_room_in_real_sqlite():
repo = SqliteBookingRepository(sqlite3.connect(":memory:"))
service = make_service(repo)
booking = service.book(FOCUS, ANA, START, END)
found = repo.find_by_room("focus")
assert [b.id for b in found] == [booking.id]
Léela como una integración bien montada. El repositorio es real —SqliteBookingRepository, la costura bajo prueba—; el pago, el correo y el reloj están doblados en el helper make_service. El primer test verifica el flujo book→get: reserva, lee de vuelta, comprueba que los campos que cruzan la costura llegaron intactos. El segundo verifica find_by_room: que la reserva no solo se guardó, sino que es localizable por su sala en la base de datos real. Ambos verifican contra el repositorio real, que es donde la costura vive.
Qué esperar. En mi máquina (Python 3.14.0, pytest 9.1.1):
python3 -m pytest tests/test_reservo_integration.py -v
============================= test session starts ==============================
platform darwin -- Python 3.14.0, pytest-9.1.1, pluggy-1.6.0
collected 2 items
tests/test_reservo_integration.py::test_book_persists_a_confirmed_booking_in_real_sqlite PASSED [ 50%]
tests/test_reservo_integration.py::test_the_booking_is_findable_by_room_in_real_sqlite PASSED [100%]
============================== 2 passed in 0.02s ===============================
Dos verdes en 0.02s. La integración book→get funciona: BookingService y SqliteBookingRepository, los dos reales, colaboran a través de la costura del repositorio. La reserva que el servicio creó se escribió en una tabla de SQLite, se leyó de vuelta con sus campos intactos, y es localizable por su sala. Con esto tienes los entregables 1 y 4. Faltan los que pesan: el mapa de decisiones y la afirmación del verde.
Solución de referencia
Ver la solución completa (decisiones + afirmación del verde)
Entregable 2 — el mapa de decisiones de doblado. Para cada colaborador, real o doblado, y la razón con la regla de la lección 4:
| Colaborador | Real o doblado | Por qué |
|---|---|---|
repo (BookingRepository) | Real (SqliteBookingRepository) | Es la costura bajo prueba. La primera mitad de la regla es no negociable: la junta que integras va real, o no estás integrando nada. Doblarla sería un unit test disfrazado. |
payments (PaymentGateway) | Doblado (StubPaymentGateway) | Externo con efectos: un cobro real movería dinero. Además no es la costura bajo prueba. Se dobla; el stub deja pasar (ok=True) y de paso registra el cobro si quisiéramos verificarlo. |
emails (EmailSender) | Doblado (SpyEmailSender) | Externo con efectos: un correo real llena una bandeja y depende de un servidor. No es la costura bajo prueba. Se dobla; el spy registra el envío sin mandarlo. |
clock (Clock) | Doblado (FixedClock) | No determinista: el reloj real haría el test flaky. book casi no lo usa, pero congelarlo mantiene todo determinista y prepara el terreno si el flujo creciera hacia cancel. |
calendar (Calendar) | Real (en memoria) | Ni lento, ni no determinista, ni externo: no cae en ninguna categoría de la regla. Dejarlo real no cuesta nada y no es la costura bajo prueba, así que es indiferente; lo dejamos real por simplicidad. |
La regla de oro aplicada: real la costura que pruebas (repo), doblado lo caro que no es esa costura (payments, emails, clock), indiferente lo barato y sin frontera (calendar).
Entregable 3 — la afirmación del verde. Lo que la suite en verde garantiza: que BookingService y SqliteBookingRepository, las piezas reales de producción, colaboran correctamente en el flujo book→get —una reserva creada por el servicio se escribe en la base de datos de verdad, se lee de vuelta con su id, room_id, status confirmado y price_cents intactos, y es localizable por su sala—. No es una afirmación condicional sobre un doble (como la de un unit test); es un hecho sobre las piezas reales cruzando su costura.
Lo que el verde no garantiza, y conviene decir explícitamente: (a) no verifica el start como datetime —lo omitimos porque sabemos que el repositorio buggy lo devuelve como str; verificarlo, o ejercerlo en cancel, es el bug de la lección 6—; (b) no prueba la persistencia física entre reinicios del proceso —usamos :memory:, así que la base vive mientras la conexión vive; probar que sobrevive a cerrar y reabrir un archivo es otro test, el de la lección 3—; (c) no cubre los caminos de fallo (sala ocupada, pago rechazado) —esos son unit tests, más baratos—. La suite prueba justo lo que declara: el flujo feliz de book→get a través de la costura real, y nada más. Un buen entregable nombra sus límites tanto como sus garantías.
La suite es la del ejemplo trabajado, verbatim. Corre con python3 -m pytest tests/test_reservo_integration.py -v y da los dos verdes que ya viste. Cada aserción mira la base de datos real a través del repositorio, que es donde la costura bajo prueba deja su huella; ningún colaborador caro se dejó real de más, ninguna aserción se ató a algo que no cruza la costura. Esa correspondencia limpia —real la costura, doblado lo caro, verificado lo que cruza— más el razonamiento que la justifica, es el mini-proyecto entregado.
Errores comunes
Entregar la suite verde sin el mapa de decisiones. Qué pasa: alguien enchufa el repositorio real, ve los dos PASSED y da el proyecto por hecho, sin justificar qué dejó real y qué dobló. Por qué pasa: el verde "se siente" como la entrega. Cómo detectarlo: si no puedes escribir, para cada colaborador, una frase que empiece con "lo dejé real / lo doblé porque...", te falta el entregable central. Cómo corregirlo: el mini-proyecto evalúa el criterio, no el verde; el verde es la evidencia, el mapa de decisiones es la tesis. Justifica cada elección con la regla de la lección 4.
Doblar la costura que dices integrar. Qué pasa: alguien, por costumbre o por velocidad, le pasa un FakeBookingRepository a la suite y la llama "integración de book→get". Por qué pasa: el fake es más rápido y el reflejo de doblar es fuerte. Cómo detectarlo: mira qué pieza está en la costura que el título dice probar. Si es un doble, no integras esa costura —tienes un unit test—. Cómo corregirlo: la costura bajo prueba va real, sin excepción. Si lo que querías era un unit test, está bien, pero no lo llames integración ni creas que probaste la junta con la base de datos.
Dejar reales el pago o el correo "para probar más". Qué pasa: alguien, con la idea de que más realidad es mejor, deja el PaymentGateway o el EmailSender reales además del repositorio. Por qué pasa: la intuición de que más real es más confianza. Cómo detectarlo: si tu integración cobra tarjetas, manda correos o tarda, dejaste real algo que la regla manda doblar. Cómo corregirlo: la integración de esta costura solo necesita el repositorio real; el pago y el correo no son la junta bajo prueba y son externos con efectos, así que se doblan. Probar más juntas en un test no es virtud: es costo y fragilidad de más.
Ejercicios
Ejercicio 1 — Añade el flujo de cancel a tu suite. Extiende la suite de referencia con un test de integración de book→cancel→get contra el repositorio real. Piensa: ¿pasará con el repositorio buggy de este módulo, o hará falta el arreglo? Describe qué esperas y por qué.
Ver solución
Con el repositorio buggy (el que devuelve start como str), el test de book→cancel→get fallaría —igual que en la lección 6—, con un TypeError: unsupported operand type(s) for -: 'str' and 'datetime.datetime'. La razón: cancel lee la reserva de vuelta (con start como texto), y refund_cents intenta booking.start - now, que revienta al restar un str de un datetime. No es un problema del test; es el bug real que la integración caza.
Para que pase, hace falta el arreglo de la lección 6: que SqliteBookingRepository.get convierta el texto de vuelta con datetime.fromisoformat, para que start vuelva como datetime y la aritmética de cancel funcione. Con el repositorio arreglado, el test de book→cancel→get pasaría, verificando el reembolso correcto (según la anticipación del reloj) y el status cancelado releído.
El test se vería así (contra el repositorio arreglado):
def test_book_cancel_get_full_flow():
repo = SqliteBookingRepository(sqlite3.connect(":memory:")) # el arreglado
service = make_service(repo) # CLOCK = 9 dias antes
booking = service.book(FOCUS, ANA, START, END)
refund = service.cancel(booking.id)
assert refund == 6000 # reembolso completo
assert repo.get(booking.id).status == "cancelled"
Lo que esto demuestra: añadir cancel al flujo lo vuelve un test amplio que cruza más de la costura —escribir, leer, recalcular, reescribir, releer— y por eso caza el bug del datetime que el book→get solo (que no usa el start en aritmética) no tocaba. El alcance del flujo determina qué bugs puede cazar.
Ejercicio 2 — Verifica la escritura con SQL crudo. Añade a tu suite un test que verifique la fila con un SELECT directo sobre la tabla, sin pasar por repo.get. Explica qué bug cazaría este test que los del ejemplo trabajado no.
Ver solución
El test consultaría la tabla directamente, como en la lección 3:
def test_book_writes_the_expected_row():
conn = sqlite3.connect(":memory:")
repo = SqliteBookingRepository(conn)
service = make_service(repo)
booking = service.book(FOCUS, ANA, START, END)
rows = conn.execute(
"SELECT room_id, status, price_cents FROM bookings WHERE id = ?",
(booking.id,),
).fetchall()
assert len(rows) == 1
assert rows[0] == ("focus", "confirmed", 6000)
Este test caza bugs que los del ejemplo trabajado no ven: los errores simétricos entre save y get. Los tests del ejemplo verifican con repo.get(...), que corre el mismo código de lectura del repositorio; si save y get compartieran un error —por ejemplo, escribir y leer el status de una columna equivocada de forma consistente—, el ida-y-vuelta lo cancelaría y repo.get devolvería un valor correcto pese a que la fila en la tabla está mal. El SELECT crudo mira la costura desde afuera, con una herramienta ajena al repositorio, así que ve la fila tal como quedó y delata esos errores. No hace falta en cada test, pero tener al menos uno por costura te protege del punto ciego de verificar una escritura solo con el código que la hizo.
Ejercicio 3 — Justifica :memory: contra disco para tu entrega. La suite de referencia usa sqlite3.connect(":memory:"). Explica por qué es la elección correcta para este mini-proyecto y en qué caso deberías cambiar a un archivo en disco.
Ver solución
:memory: es la elección correcta porque el mini-proyecto prueba la costura BookingService↔repositorio —la serialización, el esquema, el ida-y-vuelta— y para eso :memory: es SQLite de verdad: parsea el SQL, crea la tabla, aplica los tipos y las restricciones exactamente como en disco. Da toda la confianza sobre la costura del repositorio, y es unas cincuenta veces más rápido que el disco (lección 7), sin la carga de crear y borrar un archivo. Para el flujo book→get, no necesitas más.
Deberías cambiar a un archivo en disco solo si lo que quieres probar es específicamente la persistencia física: que una reserva sobrevive a cerrar la conexión y reabrir la base con una conexión nueva, como en la lección 3. Eso es imposible de verificar en :memory: —la base vive solo mientras la conexión vive—, así que ahí el disco compra una confianza que :memory: no puede dar, y vale su costo. Pero para el resto de las integraciones del repositorio, :memory: es el default correcto: real donde importa (la lógica de SQLite), barato donde no importa (el viaje al disco físico). Elegir el recurso según lo que el test necesita demostrar es parte del criterio que este módulo te dejó.
Resumen y cierre del módulo
Con este mini-proyecto entregado, cierras el módulo 5. Escribiste con tus manos una prueba de integración real: BookingService sobre SqliteBookingRepository, el flujo book→get verificado contra una base de datos de verdad, en verde, con find_by_room como segunda cara de la costura. Y —lo que el proyecto evalúa— justificaste cada decisión: el repositorio real porque es la costura bajo prueba, el pago, el correo y el reloj doblados por la regla de la lección 4, y la afirmación precisa de qué garantiza tu verde y qué deja fuera. Entregaste la prueba en carretera, no solo el simulador.
Recorriste el módulo entero: el salto de certificar cada pieza a verlas funcionar juntas (lección 1); la definición precisa de una integración de verdad, por su costura (lección 2); la costura BookingService↔SQLite hecha tangible, con la fila en la tabla y la persistencia en disco (lección 3); la regla de oro de qué doblar y qué mantener real (lección 4); el vocabulario solitario contra sociable (lección 5); la recompensa, la integración cazando el datetime→str en el flujo completo con un TypeError (lección 6); y el costo medido, que le da forma a la pirámide (lección 7). Sales con tu primera integración real escrita y con el criterio para todas las que vienen.
Hacia dónde sigue la guía. Este módulo te dio la integración como concepto y la primera prueba real; los dos que siguen la afinan. El módulo 6 te lleva a las fronteras específicas a fondo: una transacción de SQLite con su commit y su rollback, un archivo real, una llamada HTTP a un http.server de la stdlib, y cómo hacer todo eso rápido y determinista —el manejo experto de los recursos que aquí usamos de forma básica—. El módulo 7 toma los datos y el aislamiento: el rollback para aislar tests, fixtures que crean y destruyen una base temporal, y cómo mantener las pruebas de integración independientes y repetibles cuando comparten estado real —la solución a la carga de sembrar y limpiar que la lección 7 te dejó sintiendo—. La integración que hoy escribiste en su forma básica, la vas a llevar a las fronteras difíciles y al estado bajo control.
Recursos
- Documentación de pytest — Cómo invocar pytest (
-v) — la referencia para correr tu suite con-vy producir tu propio bloque "Qué esperar" con losPASSED, como en el ejemplo trabajado. sqlite3— DB-API para SQLite (documentación de Python) — la referencia del recurso real que integras;connect(":memory:"),executeyfetchall, las piezas con las que el repositorio guarda y recupera, y con las que verificas la fila con SQL crudo.- Martin Fowler — IntegrationTest — el marco que define qué prueba una integración y que respalda la afirmación del verde del entregable 3: un hecho sobre la colaboración real, no una suposición sobre un doble.
testing-backend-applications-guide— la guía hermana que retoma donde el módulo 6 llega: probar una app web completa con framework, rutas y HTTP de punta a punta, el nivel de integración que esta guía deja marcado en la frontera.