Módulo 1: De la unidad a la integración: por qué

4. Las costuras: dónde se conectan los componentes

Descripción

La pirámide te dijo que quieres algunos tests de integración para "las costuras que importan". Esta lección responde la pregunta que quedó abierta: ¿qué es exactamente una costura, cómo la reconoces en el código y cómo decides qué hacer con ella? La palabra viene de la sastrería —el punto donde dos piezas de tela se unen— y en software significa lo mismo: una costura es un punto donde dos componentes se conectan, y donde, por eso, puedes intervenir. Es la juntura que un test de integración prueba y que un doble sustituye.

Ya conoces la costura más importante de Reservo sin haberla nombrado así: el parámetro repo del constructor de BookingService. BookingService no crea su repositorio adentro; lo recibe. Ese punto de recepción es una costura, porque ahí puedes conectar cualquier objeto que cumpla la interfaz BookingRepositorysave, get, find_by_room—: el FakeBookingRepository en un unit test, el SqliteBookingRepository en producción o en un test de integración. La costura es lo que hace posibles ambos mundos. Y ahí está su doble filo: el mismo punto que te deja enchufar un doble para aislar es el punto donde el doble y lo real pueden divergir sin que nadie lo note. La costura es a la vez tu oportunidad de doblar y tu riesgo de que el doble mienta.

Conexión con el módulo: esta lección le da nombre y anatomía al lugar donde vive todo lo que la guía enseña. La lección 2 definió integración como "cruzar la costura con una pieza real"; aquí ves qué es esa costura y aprendes a distinguir las que están dentro del proceso (la interfaz BookingRepository) de las que están en una frontera del mundo (el disco, la red). La lección 5 mostrará una divergencia concreta en una costura; el contrato de los módulos 3 y 4 será, precisamente, la forma de mantener honestos a los dos lados de una costura; y las fronteras reales del módulo 6 son las costuras más peligrosas de todas. Reconocer una costura es reconocer dónde puede fallar la integración.

Analogía: los enchufes de tu casa

Piensa en los enchufes de la pared. Cada enchufe es una costura entre dos mundos: la instalación eléctrica de la casa, por un lado, y cualquier aparato que conectes, por el otro. El enchufe existe justamente para que esos dos mundos no estén soldados: puedes desconectar la licuadora y conectar el cargador, y la pared no se entera —le da corriente a lo que sea que enchufes—. Esa separación es lo que hace la casa flexible. Pero el enchufe también es donde las cosas pueden no encajar: llevas tu cargador de viaje a otro país y el enchufe tiene otra forma, u otro voltaje, y aunque "es un enchufe" y "es un cargador", no funcionan juntos —o peor, encajan pero el voltaje quema el aparato—. El contrato del enchufe (la forma de las clavijas, los 120 voltios) es lo que garantiza que cualquier aparato que lo respete funcione; cuando un lado supone 120 y el otro entrega 240, el enchufe encaja y el sistema falla.

La costura repo de BookingService es ese enchufe. BookingService es la pared: le da corriente a "lo que sea que enchufes" mientras cumpla la forma save/get/find_by_room. El FakeBookingRepository y el SqliteBookingRepository son dos aparatos distintos que caben en el mismo enchufe. Y la divergencia del datetime de la lección 1 es exactamente el problema del voltaje: los dos aparatos encajan en el enchufe (ambos tienen get), pero uno devuelve un datetime y el otro un str —el mismo enchufe, distinto voltaje—, y el aparato que esperaba datetime se "quema". Un unit test prueba tu aparato contra un enchufe que tú fabricaste con el voltaje que supones; un test de integración lo prueba contra el enchufe de verdad, con el voltaje real.

Anatomía de una costura

Una costura tiene siempre dos lados y un contrato entre ellos.

  • El consumidor (consumer): el lado que usa la costura. En Reservo, BookingService es el consumidor del repositorio: llama a repo.save(...) y repo.get(...) esperando cierto comportamiento.
  • El proveedor (provider): el lado que cumple la costura. FakeBookingRepository y SqliteBookingRepository son dos proveedores del mismo repositorio: cada uno implementa save, get, find_by_room a su manera.
  • El contrato: las expectativas que el consumidor tiene sobre el proveedor. Aquí, implícito por ahora: "save(b) guarda; get(b.id) devuelve la misma reserva; get de un id ausente lanza". Hacer explícito ese contrato y verificarlo contra ambos proveedores es, literalmente, el tema de los módulos 3 y 4.

Este vocabulario —consumer, provider, contrato— es el que vas a usar el resto de la guía. Por ahora quédate con la imagen: en toda costura hay un lado que espera algo y un lado que lo promete, y el trabajo de la integración es verificar que la promesa se cumple con la pieza real, no solo con el doble.

Dos clases de costura: en proceso y de frontera

No todas las costuras son igual de peligrosas. Conviene distinguir dos.

La costura en proceso. Une dos objetos que viven en la misma memoria de Python. La costura repo es de estas: BookingService y el repositorio son ambos objetos Python, y save/get son llamadas a métodos normales. El dato no sale del proceso; a lo sumo cambia de forma dentro de él (un datetime que se serializa a str). Son costuras relativamente dóciles: rápidas de cruzar, deterministas, y su divergencia suele ser de forma de los datos, como la del datetime.

La costura de frontera. Une tu proceso con algo fuera de él: el disco (un archivo, la base de datos SQLite en un archivo real), la red (una llamada HTTP a otro servidor), el reloj del sistema. Aquí el dato de verdad sale del proceso y vuelve, y en el viaje pueden pasar cosas que en memoria nunca pasan: el disco se llena, el archivo está bloqueado, la red se cae o tarda, la conexión se corta a la mitad de una transacción. Son las costuras más peligrosas —y las que el módulo 6 aborda de frente—, porque su divergencia no es solo de forma: es de fiabilidad.

En Reservo, SqliteBookingRepository es interesante porque toca las dos. Cuando lo usas con sqlite3.connect(":memory:"), la costura de frontera es suave (la "base de datos" vive en memoria); cuando lo usas con un archivo en disco, la frontera se vuelve real, con todo lo que eso implica. La lección 7 medirá esa diferencia. Por ahora, reconoce que la palabra "costura" cubre desde una llamada a un método en memoria hasta un salto a la red, y que el riesgo crece a medida que te alejas del proceso.

Ejemplo trabajado: una costura, dos piezas enchufadas

La mejor prueba de que la costura repo es real es enchufarle las dos piezas y ver que el mismo BookingService funciona con ambas. Este test parametrizado corre exactamente el mismo flujo —reservar Focus dos veces, luego pedir las reservas de esa sala— una vez con el FakeBookingRepository y otra con el SqliteBookingRepository real. El código del servicio no cambia ni una línea; lo único que varía es qué pieza está enchufada en la costura.

# tests/test_seam.py — la costura acepta cualquier proveedor
import sqlite3
from datetime import datetime, timedelta

import pytest

from reservo.calendar import Calendar
from reservo.doubles import (FakeBookingRepository, 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")
CLOCK = datetime(2026, 3, 1, 9)


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


def make_repos():
    return {
        "fake": FakeBookingRepository(),
        "sqlite": SqliteBookingRepository(sqlite3.connect(":memory:")),
    }


@pytest.mark.parametrize("kind", ["fake", "sqlite"])
def test_same_service_plugs_into_either_repo(kind):
    repo = make_repos()[kind]
    service = make_service(repo)
    d1 = datetime(2026, 3, 10, 9)
    d2 = datetime(2026, 3, 11, 9)
    service.book(FOCUS, ANA, d1, d1 + timedelta(hours=1))
    service.book(FOCUS, ANA, d2, d2 + timedelta(hours=1))

    focus_bookings = repo.find_by_room("focus")
    assert len(focus_bookings) == 2      # la costura acepta ambas piezas

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

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

tests/test_seam.py::test_same_service_plugs_into_either_repo[fake] PASSED [ 50%]
tests/test_seam.py::test_same_service_plugs_into_either_repo[sqlite] PASSED [100%]

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

Dos verdes. El mismo servicio, el mismo flujo, dos proveedores distintos enchufados en la misma costura, y find_by_room("focus") devuelve 2 en ambos casos. Aquí la costura se comporta igual con las dos piezas, porque find_by_room solo compara room_id, que es texto y cruza la costura sin cambiar de forma. Es la cara amable de la costura: la que te deja doblar sin miedo cuando el contrato de verdad se cumple igual en ambos lados. La cara peligrosa la viste en la lección 1 y la verás de nuevo en la 5: la misma costura, pero pidiéndole al proveedor un datetime, donde el fake y el real dejan de comportarse igual. La costura es una sola; que sea amable o peligrosa depende de si el contrato aguanta en el punto exacto que le pides.

Fíjate en el detalle técnico que hace todo esto posible: make_service(repo) acepta cualquier objeto con save/get/find_by_room. Python no exige que declares una interfaz formal; le basta con que la pieza tenga los métodos (lo que se llama duck typing). Esa flexibilidad es la costura hecha código: el enchufe que no pregunta qué aparato eres, solo si tienes las clavijas correctas.

Por qué toda costura es oportunidad y riesgo a la vez

Vale la pena detenerse en la doble naturaleza de la costura, porque es la tensión que organiza la guía entera.

La costura es tu oportunidad de doblar. Sin una costura, no podrías aislar nada. Si BookingService creara su repositorio adentro (self._repo = SqliteBookingRepository(...) en el constructor), no habría dónde enchufar un fake: la pieza real estaría soldada, y todo test tocaría la base de datos. La costura —recibir el repositorio de afuera— es lo que abre el hueco donde el doble entra. Cada costura es, literalmente, un lugar donde puedes elegir "aquí doblo" (unit test) o "aquí dejo lo real" (integración). Más costuras, más control sobre qué aíslas y qué pruebas de verdad.

La costura es tu riesgo de divergencia. Pero cada costura donde doblas es una costura donde tu doble podría comportarse distinto de lo real, y donde un unit test —que solo ve el doble— sería incapaz de notarlo. La costura no solo abre el hueco para el doble; abre el hueco para la mentira del doble. Cuantas más costuras dobles, más lógica pruebas rápido y aislado, pero más juntas quedan sin verificar contra lo real. Por eso la pirámide no es "dobla todo": es "dobla en la base para tener velocidad, e integra en el medio las costuras cuyo riesgo de divergencia no puedes ignorar".

La conclusión práctica: para cada costura, hazte dos preguntas. Primero, "¿necesito aislar aquí para tener velocidad y precisión?" —si sí, dobla, y tendrás unit tests—. Segundo, "¿el riesgo de que mi doble diverja de lo real es real e importante?" —si sí, escribe también un test de integración que cruce esta costura con la pieza de verdad—. Casi siempre la respuesta a las dos es "sí", y por eso quieres unit tests y de integración sobre la misma costura: los primeros por velocidad, los segundos por verdad. El contrato de los módulos 3 y 4 es la herramienta que hace esa segunda verificación sistemática, en lugar de dejarla al azar.

Errores comunes

No ver la costura porque la pieza está soldada. Qué pasa: alguien tiene un BookingService que crea su repositorio adentro y concluye "aquí no hay nada que doblar, no hay costura". Por qué pasa: sin inyección, la costura está tapada, y lo que no se ve parece que no existe. Cómo detectarlo: si no puedes escribir un unit test sin tocar la base de datos, es que una costura que debería estar abierta está soldada. Cómo corregirlo: abre la costura con inyección de dependencias —recibe el repositorio por el constructor en vez de crearlo adentro—. Eso es materia de la guía de dobles; aquí basta reconocer que una costura tapada es una integración forzada, no la ausencia de costura.

Confundir "encaja" con "cumple el contrato". Qué pasa: alguien ve que el fake y el real tienen ambos get, concluye que son intercambiables, y se sorprende cuando divergen. Por qué pasa: que dos piezas quepan en la misma costura (tengan los métodos) se siente como que se comportan igual. Cómo detectarlo: es justo el bug del datetime: los dos tienen get, encajan en la costura, y devuelven tipos distintos. Encajar es la forma de las clavijas; cumplir el contrato es el voltaje. Cómo corregirlo: no basta con que la pieza tenga los métodos; el comportamiento de esos métodos debe coincidir con lo que el consumidor espera. Verificar eso es contract testing (módulos 3-4); reconocer que "encaja" y "cumple" son cosas distintas es el primer paso.

Tratar todas las costuras como igual de riesgosas. Qué pasa: alguien escribe tests de integración para cada costura por igual, o para ninguna por igual, sin distinguir. Por qué pasa: "costura" suena a una sola cosa. Cómo detectarlo: si gastas tanto esfuerzo de integración en una costura en proceso trivial como en la frontera con la base de datos, estás mal calibrado. Cómo corregirlo: distingue las costuras en proceso (dóciles, divergencia de forma) de las de frontera (peligrosas, divergencia de fiabilidad). Invierte tu presupuesto de integración donde el riesgo es mayor: las fronteras con el disco y la red, no cada llamada a un método en memoria.

Ejercicios

Ejercicio 1 — Encuentra las costuras. En BookingService(calendar, clock, payments, emails, repo), lista las costuras que hay y clasifica cada una como "en proceso" o "de frontera" cuando usa la pieza real de producción. Para cada una, nombra al consumidor y al proveedor.

Ver solución

BookingService tiene cinco costuras, una por cada colaborador que recibe:

  • calendar — en proceso. Consumidor: BookingService. Proveedor: Calendar (lógica en memoria). El calendario no sale del proceso.
  • clock — de frontera (leve). Consumidor: BookingService. Proveedor: el reloj del sistema en producción (datetime.now()), que es una llamada al sistema operativo. Su "peligro" es el no determinismo, no la fiabilidad.
  • payments — de frontera. Consumidor: BookingService. Proveedor: la pasarela de pago real, que vive en otro servidor: una costura de red, cara y falible.
  • emails — de frontera. Consumidor: BookingService. Proveedor: el servidor de correo real: otra costura de red.
  • repo — de frontera. Consumidor: BookingService. Proveedor: SqliteBookingRepository sobre un archivo real: costura de disco (y en proceso cuando la base vive en :memory:).

La observación clave: casi todas las costuras de BookingService son de frontera en producción, porque casi todos sus colaboradores viven fuera del proceso. Por eso BookingService es un festival de dobles en los unit tests y un buen candidato a tests de integración en sus costuras de más riesgo (el repo, sobre todo).

Ejercicio 2 — Encaja pero no cumple. Un compañero escribe un BrokenRepository cuyo get siempre devuelve None en vez de lanzar cuando el id no existe. Enchúfalo en la costura repo de BookingService. ¿El código corre (encaja)? ¿Cumple el contrato? ¿Dónde y cuándo estallaría la diferencia?

Ver solución

Encaja: sí. BrokenRepository tiene save, get y find_by_room, así que Python lo acepta en la costura sin protestar —tiene las clavijas—. BookingService lo construye y lo usa sin error de construcción.

Cumple el contrato: no. El contrato implícito del repositorio dice "get de un id ausente lanza (KeyError/NotFound)". Este proveedor devuelve None en su lugar. Encaja en la forma, pero viola el comportamiento esperado —el problema del voltaje otra vez—.

Dónde y cuándo estalla: en cancel, que hace booking = self._repo.get(booking_id) y luego usa booking.price_cents y booking.status. Con un id válido, todo bien. Con un id ausente, un repo que cumple el contrato lanzaría ahí mismo, con un error claro; este devuelve None, y el código sigue hasta None.price_cents, que estalla con un AttributeError confuso, lejos de la causa real. Peor aún: un unit test que use un fake correcto (que sí lanza) nunca vería este bug, porque el bug vive en este proveedor, en esta costura. Es, exactamente, el tipo de divergencia que el módulo 2 disecciona y que el contrato de los módulos 3-4 previene.

Ejercicio 3 — ¿Doblar o integrar esta costura? Para cada costura de Reservo, decide si en tu suite querrías doblarla (unit test), integrarla con la pieza real (test de integración), o ambas, y da una razón de una frase: (a) payments en el camino feliz de book; (b) repo para verificar que una reserva se guarda y se lee bien; (c) calendar para probar la regla de solapamiento.

Ver solución
  • (a) payments — doblar (unit). El proveedor real cobra dinero de verdad y vive en la red: jamás lo quieres en tu suite. Se dobla siempre con un stub/spy. (El "¿el pago real funciona?" es otra clase de prueba, con un entorno de sandbox del proveedor, y queda fuera de esta guía.)
  • (b) repo — ambas. Doblar en los unit tests de la lógica de book/cancel (velocidad, precisión, la base de la pirámide) y integrar con el SqliteBookingRepository real en unos pocos tests (verificar que la persistencia de verdad no diverge, como el datetime). Es la costura de más valor para tener las dos.
  • (c) calendar — doblar/directo (unit). Calendar es lógica pura en memoria: no hay frontera, no hay recurso externo, no hay riesgo de divergencia entre un doble y "lo real" (el Calendar real ya es barato y determinista). Se prueba directo, sin integración; sería malgastar el nivel intermedio.

El criterio que aplicas: dobla siempre las costuras caras o peligrosas de tocar (payments, emails); integra las costuras cuyo proveedor real puede divergir de forma que importe (repo); y no molestes al nivel de integración con costuras que ya son puras y baratas (calendar). La costura del repositorio es la estrella de esta guía justamente porque es la que más pide ambas.

Resumen y siguiente paso

En esta lección le pusiste nombre y anatomía al lugar donde vive la integración: la costura, el punto donde dos componentes se conectan. Aprendiste sus tres partes —consumidor, proveedor, contrato— y el vocabulario que usarás el resto de la guía; distinguiste las costuras en proceso (dóciles, divergencia de forma) de las de frontera (peligrosas, divergencia de fiabilidad); y viste, con las dos piezas enchufadas en la costura repo, que la misma costura es a la vez tu oportunidad de doblar y tu riesgo de que el doble mienta. La regla que te llevas: para cada costura, pregunta si necesitas aislar (dobla) y si el riesgo de divergencia importa (integra también).

Antes de avanzar deberías poder: señalar las costuras de un servicio y nombrar su consumidor, proveedor y contrato; distinguir "encaja" de "cumple el contrato"; y decidir, para una costura dada, si la doblas, la integras o ambas.

Has visto la costura amable, donde el fake y el real coinciden, y te he prometido dos veces la costura peligrosa, donde divergen. Es hora de mirarla de frente y con calma. La lección 5 toma el problema central de toda la guía —un unit test verde puede esconder una integración rota— y lo demuestra en pequeño, con la costura repo pidiendo un datetime: el fake pasa, el real falla, y entiendes exactamente por qué el verde mentía.

Recursos