Módulo 5: Integración de verdad: componentes reales juntos

1. Presentación del módulo: ahora sí, las piezas juntas

Descripción

Llegaste al módulo que le da nombre a media guía. Hasta aquí hiciste un trabajo enorme sin todavía juntar dos piezas reales. El módulo 1 te mostró la brecha —un unit test verde puede esconder una integración rota— y el 2 la afiló con una divergencia concreta. Los módulos 3 y 4 te dieron el contrato: una batería compartida que corres contra el FakeBookingRepository y contra el SqliteBookingRepository, y que grita en rojo si el fake miente sobre cualquier cláusula que hayas escrito. Con eso ya no rezas para que el doble se comporte como el real: lo verificas. Pero fíjate en qué hace, exactamente, un contrato. Corre el fake solo, verifica sus cláusulas; corre el real solo, verifica las suyas. Certifica cada pieza por separado, contra un spec. Nunca puso a BookingService a hablar con el repositorio real en un flujo vivo.

Eso es lo que falta, y es precisamente lo que significa la palabra integración: conectar dos componentes reales y verlos funcionar juntos, cruzando la costura, en un flujo de verdad. No "el servicio contra un doble" ni "el repositorio contra su spec": el servicio y el repositorio, los dos reales, colaborando. Es la diferencia entre certificar por separado que el enchufe cumple la norma y que el cargador cumple la norma, y enchufar el cargador de verdad en el tomacorriente de verdad y ver si carga el teléfono. Este módulo es enchufar. Vas a escribir tu primera prueba de integración real —BookingService con el SqliteBookingRepository—, una reserva que el servicio crea, se escribe en una tabla de SQLite y se lee de vuelta. Y vas a ver, con salida de pytest, algo que ningún módulo anterior te mostró: el flujo completo cazando un bug que el contrato aislado, si tuvo un hueco, dejó pasar.

Conexión con el módulo: esta lección es el mapa del territorio que vas a recorrer. Aquí conoces el salto conceptual —de "cada pieza certificada" a "las piezas trabajando juntas"—, ves un primer bookget real en verde para que sepas hacia dónde vamos, y recibes el mapa de las ocho lecciones y la frontera con lo que sigue. La lección 2 clava la definición de "prueba de integración de verdad"; la 3 arma la primera a fondo; la 4 te da la regla de qué doblar y qué no; la 5 nombra solitario contra sociable; la 6 cobra la recompensa —la integración cazando el datetimestr en el flujo completo—; la 7 mide su costo. La frontera dura: las fronteras específicas —transacciones, archivos, HTTP— son el módulo 6, y los datos y el aislamiento —rollback, fixtures de recursos— son el módulo 7. Aquí instalamos la integración como concepto y escribimos la primera prueba real.

Analogía: certificar las piezas contra armar el mueble

Piensa en un mueble que compras por partes para armar en casa. La fábrica hace un control de calidad impecable: cada tornillo cumple su norma de rosca, cada tabla tiene el grosor especificado, cada bisagra aguanta los ciclos de apertura que promete. Certifican pieza por pieza, cada una contra su ficha técnica, y todas pasan. Eso es exactamente lo que hace un contrato: verifica que cada componente cumple su especificación, aislado. Y sin embargo, cualquiera que haya armado un mueble sabe que las piezas certificadas no garantizan un mueble que se pare. El agujero de la tabla A no queda alineado con el de la tabla B; el tornillo cumple su norma pero es dos milímetros más largo que el grosor de la madera y asoma por el otro lado; la bisagra perfecta no cierra porque la puerta y el marco, cada uno correcto, juntos no encajan. El problema nunca está en una pieza: está en el momento en que dos piezas se juntan y descubres que sus superficies de contacto no coinciden.

Armar el mueble —meter el tornillo real en el agujero real de la tabla real— es la prueba de integración. No prueba las piezas; eso ya lo hizo el control de calidad de fábrica. Prueba las uniones: que la reserva que BookingService crea entra de verdad en la columna de la tabla de SQLite, que el datetime que una pieza escribe la otra lo puede leer y usar, que el flujo completo se para sobre sus juntas. En este módulo dejas de leer fichas técnicas y agarras el destornillador: enchufas el servicio real al repositorio real y ves si el mueble se sostiene.

De vuelta a Reservo, con el repositorio real enchufado

Recordemos las dos piezas que vamos a juntar. El orquestador no cambió desde la guía de dobles: BookingService(calendar, clock, payments, emails, repo) coordina sus colaboradores. book valida disponibilidad, cobra, guarda y confirma; cancel lee la reserva, calcula el reembolso con el reloj, reembolsa, guarda el estado cancelado y avisa. Los números-ancla de siempre: Focus cuesta 2500 centavos la hora; tres horas para la socia pro Ana (20% de descuento) cuestan 6000 centavos; al cancelar, refund_cents devuelve 6000 a 72 h del inicio, 3000 a 36 h y 0 a 12 h.

La otra pieza es el repositorio real, el que ya conociste en el módulo 1 y que el contrato de los módulos 3 y 4 certificó junto al fake:

# reservo/sqlite_repo.py — el repositorio REAL, con sqlite3 de la stdlib
from reservo.models import Booking

SCHEMA = """
CREATE TABLE IF NOT EXISTS bookings (
    id          TEXT PRIMARY KEY,
    room_id     TEXT NOT NULL,
    member_id   TEXT NOT NULL,
    start       TEXT NOT NULL,
    end         TEXT NOT NULL,
    status      TEXT NOT NULL,
    price_cents INTEGER NOT NULL
)
"""


class SqliteBookingRepository:
    def __init__(self, connection):
        self._conn = connection
        self._conn.execute(SCHEMA)

    def save(self, booking):
        self._conn.execute(
            "INSERT INTO bookings "
            "(id, room_id, member_id, start, end, status, price_cents) "
            "VALUES (?, ?, ?, ?, ?, ?, ?) "
            "ON CONFLICT(id) DO UPDATE SET "
            "room_id=excluded.room_id, member_id=excluded.member_id, "
            "start=excluded.start, end=excluded.end, "
            "status=excluded.status, price_cents=excluded.price_cents",
            (
                booking.id, booking.room_id, booking.member_id,
                booking.start.isoformat(), booking.end.isoformat(),
                booking.status, booking.price_cents,
            ),
        )
        self._conn.commit()

    def get(self, booking_id):
        row = self._conn.execute(
            "SELECT id, room_id, member_id, start, end, status, price_cents "
            "FROM bookings WHERE id = ?",
            (booking_id,),
        ).fetchone()
        if row is None:
            raise KeyError(booking_id)
        return Booking(
            id=row[0], room_id=row[1], member_id=row[2],
            start=row[3], end=row[4],       # sale como str, no como datetime
            status=row[5], price_cents=row[6],
        )

    def find_by_room(self, room_id):
        rows = self._conn.execute(
            "SELECT id, room_id, member_id, start, end, status, price_cents "
            "FROM bookings WHERE room_id = ?",
            (room_id,),
        ).fetchall()
        return [Booking(*r) for r in rows]

Es sqlite3, que viene con Python, sin ninguna dependencia externa. Guarda cada reserva como una fila de una tabla de texto y números. Deja anotada, para más adelante, la misma línea que fue toda la historia del módulo 1: en get, start=row[3] sale como texto, un str, porque save lo guardó con .isoformat() y nadie lo convierte de vuelta. En este módulo, esa costura por fin va a ejercerse en un flujo completo, y vas a ver qué pasa cuando otra pieza real intenta usar ese str.

Ejemplo trabajado: tu primera integración, de un vistazo

Antes de entrar en definiciones, veamos hacia dónde vamos. Esta es una prueba de integración de verdad, la más simple que existe en Reservo: BookingService real conversando con el SqliteBookingRepository real. No hay fake en la costura que nos importa —el repositorio es SQLite de carne y hueso—. El servicio reserva; la reserva cruza la costura hacia una tabla de SQLite; y luego la leemos de vuelta con get y verificamos que el flujo completo dejó lo que debía. Los colaboradores que no son la costura bajo prueba —el pago, el correo, el reloj— siguen doblados, y en la lección 4 verás por qué eso es correcto y no una trampa.

# tests/test_book_get_integration.py — 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_then_get_persists_through_the_real_db():
    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.member_id == "m-ana"
    assert saved.status == "confirmed"
    assert saved.price_cents == 6000             # el cobro correcto, cruza intacto

Fíjate en lo que verifica y en lo que no. Comprueba que la reserva, después de escribirse en SQLite y leerse de vuelta, conserva su id, su sala, su socio, su estado confirmado y su precio de 6000 centavos. No verifica todavía el start —el campo que sabemos que cambia de forma—; a esa aserción llegaremos en la lección 6, cuando la usemos para cazar el bug. Aquí queremos lo primero que cualquiera querría de una integración: que el flujo básico —reservar y volver a leer contra la base de datos real— funciona.

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

python3 -m pytest tests/test_book_get_integration.py -v
============================= test session starts ==============================
platform darwin -- Python 3.14.0, pytest-9.1.1, pluggy-1.6.0
collected 1 item

tests/test_book_get_integration.py::test_book_then_get_persists_through_the_real_db PASSED [100%]

============================== 1 passed in 0.01s ===============================

Verde. Ahí está tu primera integración: BookingService y SqliteBookingRepository, los dos reales, funcionando juntos. La reserva que el servicio creó se escribió de verdad en una tabla de SQLite y se leyó de vuelta con los campos que importan intactos. No es un unit test —el repositorio no es un doble, es la pieza que corre en producción—; y no es un contrato —no verificamos cláusulas de una interfaz, verificamos un flujo bookget completo cruzando la costura—. Es lo que este módulo entero desarrolla, visto en un test. Todo lo que sigue es entender qué acabas de hacer, hacerlo con criterio, y cobrar lo que la integración te devuelve.

El mapa del módulo: las ocho lecciones

Vale la pena ver el recorrido, porque cada lección apoya en la anterior y todas construyen hacia la recompensa de la lección 6.

LecciónTemaLa idea en una frase
1Las piezas juntas (esta)De "cada pieza certificada por separado" a "las piezas trabajando juntas cruzando la costura"
2Qué es una integración de verdadDos o más componentes reales cruzando la costura que se prueba, no un doble a cada lado
3BookingService + SQLite juntosbook escribe en una tabla real; get la lee de vuelta; la reserva sobrevive a reabrir el archivo
4Qué doblar y qué mantener realDobla lo lento/no determinista/externo; deja real la costura que pruebas
5Solitario contra sociableTodo doblado alrededor (solitario) contra dejar reales a los vecinos (sociable)
6La integración caza lo que el unit nobookcancelget: el fake pasa, SQLite explota con un TypeError, y el arreglo lo pone en verde
7El costo de la integraciónCientos de veces más lenta en disco, y hay que sembrar y limpiar la base de datos
8Mini-proyectoEscribe la integración bookget contra SQLite real y entrega el flujo verificado en verde

Este módulo es la primera prueba de integración real y el criterio para escribirla bien. Si entiendes qué es una integración de verdad, qué dejar real y qué doblar, y por qué el flujo completo caza lo que las piezas aisladas no, los módulos 6 y 7 son afinar las herramientas para hacerlo en las fronteras difíciles y con los datos bajo control.

Lo que este módulo NO toca (la frontera)

Conviene marcar los límites desde ahora, porque hay temas vecinos que parecen de aquí y son del módulo siguiente.

Las fronteras específicas a fondo son el módulo 6. Aquí usamos SQLite como "la pieza real al otro lado de la costura del repositorio", y la ejercemos en un flujo. Pero las mañas propias de cada frontera —una transacción de SQLite con su commit y su rollback, leer y escribir un archivo real, una llamada HTTP a un http.server de la stdlib, y cómo hacer todo eso rápido y determinista— son el módulo 6. En este módulo, SQLite es el colaborador real que integramos, no el recurso cuyas fronteras estudiamos con lupa.

Los datos y el aislamiento son el módulo 7. Verás que una integración necesita sembrar la base de datos antes y dejarla limpia después, y en la lección 7 lo nombraremos como un costo. Pero las técnicas para hacerlo bien —aislar tests con rollback, fixtures que crean y destruyen una base de datos temporal, mantener las pruebas independientes y repetibles cuando comparten estado real— son el módulo 7. Aquí sembramos y limpiamos a mano lo justo para que la primera integración corra; el arte de aislar viene después.

El framework web no es de esta guía. Reservo se integra en proceso: BookingService más un SqliteBookingRepository real, y a lo sumo, en el módulo 6, un http.server mínimo de la stdlib. Probar una app web completa —con FastAPI, rutas, el ciclo petición-respuesta de punta a punta— es testing-backend-applications-guide. Cada vez que un tema roce ese borde, lo enlazamos y seguimos.

Errores comunes

Creer que "el contrato pasó en verde" ya prueba que las piezas funcionan juntas. Qué pasa: el contrato de los módulos 3 y 4 certificó el fake y el real, ocho verdes, y alguien concluye que ya no hace falta integrar. Por qué pasa: un contrato verde es una garantía fuerte y es fácil creer que cubre todo. Cómo detectarlo: pregúntate si algún test puso a BookingService a usar el repositorio real en un flujo, o si solo se verificó el repositorio contra su spec. Si nadie ejerció la colaboración, las juntas no se probaron. Cómo corregirlo: el contrato certifica cada pieza por separado; la integración verifica que colaboran. Son garantías distintas y complementarias —lo verás crudo en la lección 6, donde un contrato con un hueco deja pasar un bug que el flujo completo caza—.

Llamar "integración" a cualquier test que toque una pieza real de refilón. Qué pasa: un test dobla el repositorio pero usa un Calendar real en memoria, y alguien lo llama de integración. Por qué pasa: "usa algo real" suena a integración. Cómo detectarlo: pregúntate cuál es la costura que el test prueba y si esa costura tiene una pieza real de cada lado. Si la costura que te importa está doblada, no es una integración de esa costura, por más piezas reales incidentales que haya. Cómo corregirlo: la integración es sobre una costura concreta; nómbrala y verifica que la cruzas con la pieza real. La lección 2 afila esta distinción.

Querer integrar todo de una vez para "probar de verdad". Qué pasa: alguien, entusiasmado, arma BookingService con el pago real, el correo real y la base de datos real, y lo llama la prueba definitiva. Por qué pasa: "todo real" parece lo más honesto. Cómo detectarlo: si tu test cobra tarjetas de verdad, manda correos de verdad o tarda segundos, te pasaste de la integración a un end-to-end frágil y caro. Cómo corregirlo: una buena integración deja real solo la costura que prueba (el repositorio) y dobla lo demás (pago, correo, reloj). Esa mezcla es la lección 4, y la razón por la que existe es el costo de la lección 7.

Ejercicios

Ejercicio 1 — ¿Contrato o integración? Para cada descripción, di si es una prueba de contrato (certifica una pieza contra su spec, aislada) o de integración (dos piezas reales trabajando juntas en un flujo): (a) correr la batería de cuatro cláusulas del repositorio contra el FakeBookingRepository y contra el SqliteBookingRepository; (b) llamar a service.book(...) con un SqliteBookingRepository real y luego repo.get(...) para verificar que la reserva quedó; (c) verificar que SqliteBookingRepository.get de un id ausente lanza KeyError; (d) correr bookcancelget con BookingService y el repositorio real.

Ver solución
  • (a) Contrato. Es la batería parametrizada de los módulos 3 y 4: verifica cada implementación del repositorio contra las mismas cláusulas, por separado. No hay un flujo de BookingService cruzando la costura; hay un spec certificado contra dos providers. Es contrato puro.
  • (b) Integración. BookingService real y SqliteBookingRepository real colaboran: el servicio crea la reserva, la escribe en la base de datos de verdad, y la leemos de vuelta. Dos piezas reales cruzando la costura en un flujo. Es la integración de la lección 3.
  • (c) Contrato (o integración estrecha del repositorio contra su base de datos). Ejercita el repositorio real contra su recurso real, sin BookingService. Es una de las cláusulas del contrato; también puedes verla como la integración más estrecha posible —una pieza, una costura—. La distinción fina es de la lección 2; lo que no es, es un unit test con dobles.
  • (d) Integración. El flujo completo con dos piezas reales: book escribe, cancel lee y recalcula, get vuelve a leer, todo contra SQLite de verdad. Es la integración amplia de la lección 6, la que caza el datetimestr.

La regla que estás afinando: el contrato pregunta "¿esta pieza cumple su spec?"; la integración pregunta "¿estas dos piezas reales funcionan juntas?". Son preguntas distintas, y esta guía te da las dos herramientas para no confundirlas.

Ejercicio 2 — Qué no verifica la primera integración. El test del ejemplo trabajado comprueba id, room_id, member_id, status y price_cents, pero deliberadamente no verifica saved.start == START. Sin correr nada, explica por qué esa aserción se dejó para la lección 6 y qué crees que pasaría si la agregaras ahora.

Ver solución

Se dejó fuera porque start es el campo que sabemos que cambia de forma al cruzar la costura: save lo guarda con .isoformat() como texto, y get lo devuelve como str sin convertirlo de vuelta a datetime. Si agregaras assert saved.start == START al test del ejemplo, fallaría, porque compararía '2026-03-10T09:00:00' (un str) contra datetime(2026, 3, 10, 9, 0), y eso es False. Es exactamente la divergencia del módulo 1.

La razón de posponerla es pedagógica: esta lección quiere mostrarte una integración pasando, para que veas la forma limpia de la herramienta antes de usarla para cazar bugs. Los campos que elegimos verificar —id, sala, socio, estado, precio— son todos de tipos con equivalente nativo en SQLite (texto e int), así que cruzan la costura sin cambiar de forma y el test pasa en verde. La aserción del start, que es la que revela el bug, la reservamos para la lección 6, donde además la llevaremos al flujo completo bookcancelget para ver que el problema no es solo de forma sino de uso: cancel no puede restar un str.

Ejercicio 3 — El argumento para tu equipo. Un compañero dice: "ya tenemos el contrato de los módulos 3 y 4 en verde para el repositorio; escribir además tests de integración es duplicar trabajo". Escribe una respuesta de tres o cuatro frases que explique por qué el contrato no reemplaza a la integración, apoyándote en la analogía del mueble.

Ver solución

Una respuesta posible:

"El contrato es el control de calidad de fábrica: certifica que cada pieza cumple su ficha técnica por separado —el repositorio guarda y lee, lanza cuando falta un id, actualiza en vez de duplicar—. Pero un mueble no se cae porque una pieza sea defectuosa; se cae en las juntas, cuando el tornillo real entra en el agujero real y descubres que no alinean. La integración es armar el mueble: pone a BookingService a usar el repositorio real en un flujo bookget, y prueba la unión, que es justo lo que el contrato, verificando cada pieza aislada, no toca. No es duplicar trabajo: es probar una cosa distinta —la colaboración, no las piezas—, y en la lección 6 vamos a ver un bug que el flujo completo caza y que un contrato con un hueco deja pasar."

Lo esencial: no oponer contrato e integración, sino ubicar cada uno. El contrato garantiza que las piezas cumplen su spec; la integración garantiza que colaboran. Un equipo maduro tiene los dos, porque cada uno cubre una clase de fallo que el otro no ve.

Resumen y siguiente paso

En esta lección diste el salto que da nombre al módulo: de certificar cada pieza por separado (el contrato de los módulos 3 y 4) a verlas funcionar juntas cruzando la costura (la integración). Con el mueble armado entendiste que las piezas certificadas no garantizan un mueble que se para: las uniones fallan donde nadie las probó. Y viste tu primera integración real con salida de pytest: BookingService y SqliteBookingRepository, los dos reales, con un bookget que escribe en una tabla de SQLite y la lee de vuelta, en verde. Tienes el mapa de las ocho lecciones y la frontera con los módulos 6 (fronteras específicas) y 7 (datos y aislamiento).

Antes de avanzar deberías poder: distinguir una prueba de contrato (una pieza contra su spec) de una de integración (dos piezas reales juntas); explicar por qué el contrato no reemplaza a la integración; y reconocer qué dejamos real (el repositorio, la costura bajo prueba) y qué doblado (pago, correo, reloj) en esa primera integración, aunque el porqué a fondo sea la lección 4.

Lo que sigue es clavar la definición hasta que no quede ninguna ambigüedad. En la lección 2 vamos a decir con precisión qué es —y qué no es— una prueba de integración de verdad: por qué "tocar una pieza real" no basta si no es la costura que pruebas, y qué afirma una integración que un unit test es incapaz de afirmar. Entender esa definición con filo es lo que te deja escribir integraciones que prueban algo, en vez de tests confusos que no son ni una cosa ni la otra.

Recursos

  • Documentación de pytest — Getting Started — la puerta de entrada oficial a pytest, la herramienta con la que ejecutamos y citamos cada salida del módulo; útil para reconfirmar tu entorno (Python 3.14, pytest 9.1.1) antes de empezar.
  • sqlite3 — DB-API para SQLite (documentación de Python) — la referencia del módulo de la stdlib que hace de "componente real" al otro lado de la costura del repositorio en toda la integración; cero dependencias externas.
  • Martin Fowler — IntegrationTest — el marco que define qué es una prueba de integración y por qué el término significa cosas distintas para distintas personas; contexto para la definición que la lección 2 clava.
  • testing-backend-applications-guide — la guía hermana del otro lado de la frontera: probar una app web de verdad (framework, rutas, HTTP de punta a punta), que este módulo deja fuera y trabaja solo con los servicios en proceso más SQLite.