Módulo 1: De la unidad a la integración: por qué
7. El costo y el beneficio de los tests de integración
Descripción
Toda la guía ha empujado en una dirección: la integración caza bugs que el unit test no ve, así que necesitas integración. Es cierto, y es solo la mitad de la historia. La otra mitad, la que esta lección pone sobre la mesa sin adornos, es que un test de integración cuesta más que un unit test —más tiempo por corrida, más fragilidad, más setup— y que ese costo es la razón exacta por la que la pirámide tiene el medio angosto y no una base de pura integración. Si la integración fuera gratis, probaríamos todo contra lo real y no habría nada que decidir. No es gratis, y por eso hay una decisión que tomar en cada costura: ¿este beneficio vale este costo?
Hasta ahora "cuesta más" ha sido una afirmación cualitativa. En esta lección la medimos. Vas a ver, con números reales de mi máquina, cuántas veces más lento es el SqliteBookingRepository sobre un archivo de disco que el FakeBookingRepository en memoria, para la misma operación repetida. La cifra —cientos de veces— no es para asustarte de la integración; es para calibrar tu intuición: entender que cada test de integración que añades pesa mucho más que un unit test en el reloj de la suite, y que por eso los quieres pocos y bien elegidos, en las costuras donde el beneficio (cazar una divergencia real) justifica el costo, y no en las costuras donde un contrato o un unit test bastan.
Conexión con el módulo: esta lección cierra el "por qué" del módulo poniéndole precio a la herramienta. La lección 3 te dijo qué forma debe tener la suite (la pirámide); esta te dice por qué esa forma es económicamente forzosa, midiendo el costo que la sostiene. La 6 te dio los tipos de integración; aquí ves que los más amplios y sociables son también los más caros, lo que refuerza preferir lo estrecho. Y prepara el capstone y los módulos siguientes: cuando escribas integración de verdad (módulo 5+), sabrás administrar su costo —fixtures, aislamiento, bases en memoria— en vez de sufrirlo.
Analogía: la prueba de choque contra la simulación
Piensa en cómo una automotriz verifica la seguridad de un auto. Tiene dos herramientas. Una es la simulación por computadora: un modelo del choque corre en un servidor, miles de variantes por noche, por centavos cada una, y te dice al instante si la estructura aguanta según el modelo. La otra es la prueba de choque real: un auto de verdad, un maniquí de verdad, estrellado contra un muro de verdad a cámara lenta. Cuesta una fortuna —un auto entero destruido, un equipo, un laboratorio—, tarda semanas de preparación, y solo puedes hacer unas pocas al año. ¿Por qué no simular todo, si es tan barato? Porque la simulación es un modelo, y un modelo puede equivocarse justo donde no lo esperas —un material que se comporta distinto en el mundo real, una soldadura que el modelo idealizó—. Y ¿por qué no estrellar autos reales todo el tiempo, si son la verdad? Porque a ese costo, la fábrica quebraría antes de terminar el primer modelo.
La respuesta de la automotriz es la pirámide: miles de simulaciones baratas para explorar el espacio de diseño rápido, y unas pocas pruebas de choque reales en los puntos críticos, para confirmar que la simulación no mintió sobre lo que importa. El unit test con fake es la simulación: baratísimo, rápido, pero un modelo de lo real que puede diverger. El test de integración es la prueba de choque: caro y lento, pero la única verdad sobre cómo se comporta la pieza de verdad. Nadie estrella un auto por cada tornillo, y nadie escribe un test de integración por cada regla de negocio. La ingeniería es gastar la prueba cara donde su verdad vale el precio, y dejar que la simulación barata cubra el resto.
Ejemplo trabajado: cuánto cuesta, medido
Pongámosle número al "más lento". Este script hace lo mismo —quinientos ciclos de guardar y volver a leer una reserva— contra los dos proveedores: el FakeBookingRepository en memoria y el SqliteBookingRepository sobre un archivo real de disco, con un commit por cada guardado (como en producción). Mide el tiempo de cada uno.
# timing.py — cuánto cuesta tocar el disco de verdad
import sqlite3, tempfile, os, time
from datetime import datetime, timedelta
from reservo.doubles import FakeBookingRepository
from reservo.models import Booking
from reservo.sqlite_repo import SqliteBookingRepository
N = 500
base = datetime(2026, 3, 10, 9)
def bookings():
for i in range(N):
yield Booking(id=f"bk-{i}", room_id="focus", member_id="m-ana",
start=base + timedelta(hours=i), end=base + timedelta(hours=i + 1),
status="confirmed", price_cents=6000)
# Fake: dict en memoria
t0 = time.perf_counter()
fake = FakeBookingRepository()
for b in bookings():
fake.save(b)
fake.get(b.id)
t_fake = time.perf_counter() - t0
# SQLite en un archivo real de disco (commit por save)
path = os.path.join(tempfile.mkdtemp(), "reservo.db")
t0 = time.perf_counter()
conn = sqlite3.connect(path)
sql = SqliteBookingRepository(conn)
for b in bookings():
sql.save(b)
sql.get(b.id)
t_sqlite = time.perf_counter() - t0
conn.close()
print(f"N = {N} ciclos save+get")
print(f"FakeBookingRepository (dict en memoria): {t_fake*1000:8.2f} ms")
print(f"SqliteBookingRepository (archivo en disco): {t_sqlite*1000:8.2f} ms")
print(f"factor: {t_sqlite / t_fake:5.1f}x mas lento")
Qué esperar. En mi máquina (Python 3.14.0):
python3 timing.py
N = 500 ciclos save+get
FakeBookingRepository (dict en memoria): 0.86 ms
SqliteBookingRepository (archivo en disco): 165.20 ms
factor: 186.1x mas lento
Casi doscientas veces más lento. La cifra exacta varía entre corridas —el disco tiene su clima, y otra corrida me dio 204 ms y otra 159 ms— pero el orden de magnitud es firme: dos, tres, casi cuatro órdenes de magnitud entre "en memoria" y "en disco de verdad". ¿De dónde sale semejante diferencia? Del commit. Cada save del repositorio real hace self._conn.commit(), y un commit obliga a la base de datos a asegurarse de que el dato quedó escrito en el disco físico antes de continuar —esa garantía de durabilidad es justamente lo que hace útil a una base de datos, y es lentísima comparada con escribir en un dict de Python, que solo mueve un puntero en memoria—. El fake no tiene durabilidad que garantizar; por eso es gratis, y por eso miente sobre el costo (y, como ya sabes, a veces sobre el comportamiento).
Ahora traslada esta cifra a tu suite. Imagina los doce unit tests de la lección 3, que corrían en 0.01s. Si cada uno, en vez de calcular en memoria, tocara un archivo de disco como este, no correrían en centésimas: correrían en segundos. Multiplica por una suite de cientos de tests y entiendes por qué una base de pura integración es inviable, y por qué la pirámide tiene que ser ancha abajo (memoria, gratis) y angosta en el medio (disco, caro). El costo no es una molestia menor: es la fuerza física que le da forma a la pirámide.
El otro costo: fragilidad y setup
La lentitud es el costo más fácil de medir, pero no el único. Un test de integración trae dos cargas más que el reloj no muestra.
Fragilidad: más razones para fallar sin que tu código tenga un bug. Un unit test en memoria falla por una sola razón: tu lógica está mal. Un test de integración contra el disco puede fallar porque el archivo anterior no se borró y el id ya existía, porque el directorio temporal se llenó, porque dos tests corrieron en paralelo sobre la misma base y se pisaron, porque una conexión quedó abierta. Ninguna de esas es un bug de tu código, y todas pintan tu suite de rojo. Cada fuente de fragilidad es tiempo humano gastado en investigar un fallo que no era un fallo —el costo más caro de todos, porque erosiona la confianza en la suite (lección 3)—.
Setup: cada test de integración pide preparación y limpieza. El unit test con fake se arma en una línea: FakeBookingRepository(). El de integración necesita crear la base de datos, aplicar el esquema, y —crucial— dejarla limpia para el siguiente test, o el estado de uno contaminará al otro. Ese trabajo de crear-y-destruir recursos reales es real, y hacerlo bien (con fixtures, transacciones que se revierten, bases en memoria por test) es todo un tema —el del módulo 7 de esta guía—. Por ahora quédate con que el costo de un test de integración incluye el andamiaje de su aislamiento, no solo el tiempo de su ejecución.
Sumados, estos tres costos —tiempo, fragilidad, setup— son el "precio de la prueba de choque". No lo pagas por gusto; lo pagas cuando el beneficio lo amerita.
El beneficio, y la regla de decisión
Frente a ese costo, ¿qué compras? Una sola cosa, pero es la que ningún unit test puede darte: la verdad sobre la costura con la pieza real. El test de integración es el único que puede atrapar la divergencia del datetime, la restricción NOT NULL que solo la base impone, el id que colisiona de verdad, la transacción que no revierte. Compra confianza en las juntas, que es exactamente el hueco que la lección 1 mostró en una suite de puros unit tests verdes.
Con el costo y el beneficio sobre la mesa, la regla de decisión se vuelve concreta. Para cada costura, pregunta:
- ¿La costura tiene riesgo real de divergencia? Si es una frontera con serialización, red o disco (el repositorio, el pago, el correo), sí: la pieza real puede comportarse distinto del doble. Si es lógica pura en memoria (
Calendar,price_cents), no: no hay "pieza real distinta" que pueda divergir. Sin riesgo, no pagues integración. - Si hay riesgo, ¿un contrato basta o necesito integración de verdad? A veces la divergencia se puede fijar con un contrato —una batería de comportamiento que corres contra el fake y el real (módulos 3-4)— sin pagar el costo de la integración en cada corrida. Otras veces el riesgo vive en la frontera misma (una transacción, un archivo, HTTP) y necesitas cruzarla de verdad (módulos 5-7).
- Si necesito integración, ¿la más estrecha posible? Elige el test que ejercite la junta con el mínimo de piezas reales (lección 6): pagas menos costo por la misma verdad.
La decisión no es "integración sí o no" en abstracto; es "para esta costura, ¿el beneficio de cazar su posible divergencia justifica su costo, y cuál es la forma más barata de comprarlo?". Aplicada costura por costura, esa pregunta produce, sola, la pirámide: muchos unit tests donde no hay riesgo o donde la lógica manda, pocos tests de integración estrechos donde el riesgo de la frontera lo exige.
Errores comunes
Tratar el costo como un detalle y no como el motor de la forma. Qué pasa: alguien acepta "la pirámide" como una convención arbitraria y no entiende por qué no puede tener mil tests de integración. Por qué pasa: sin medir el costo, la pirámide parece una regla de estilo. Cómo detectarlo: si te sorprende que una suite de integración tarde minutos, no has interiorizado el factor de 200x. Cómo corregirlo: mide una vez, como en el ejemplo trabajado, y la pirámide deja de ser una convención y se vuelve aritmética: cada test de integración cuesta cientos de veces lo que un unit test, así que solo caben pocos. La forma sale del costo, no de un póster.
Pagar el costo de integración para probar lógica sin riesgo de costura. Qué pasa: alguien prueba las anclas de refund_cents contra SQLite real "para más realismo", pagando 200x por cada una. Por qué pasa: la ilusión de que lo real siempre prueba mejor. Cómo detectarlo: si un test de integración verifica algo que no depende de la pieza real (la aritmética del reembolso es idéntica con cualquier repo), estás pagando el precio de la prueba de choque para verificar algo que la simulación cubre igual de bien. Cómo corregirlo: reserva la integración para lo que solo la frontera revela. La lógica va a la base, gratis; la integración, a las costuras de riesgo, donde su verdad vale su precio.
Sufrir la fragilidad en vez de administrarla. Qué pasa: los tests de integración de alguien fallan al azar por estado compartido, y su respuesta es correrlos de nuevo hasta que pasen. Por qué pasa: parece más fácil reintentar que aislar. Cómo detectarlo: si "vuelve a correrlo" es tu arreglo habitual para un rojo de integración, estás pagando el costo de fragilidad sin administrarlo. Cómo corregirlo: la fragilidad de integración se combate con aislamiento —una base limpia por test, transacciones que revierten, fixtures que crean y destruyen el recurso—, que es exactamente el tema del módulo 7. El costo de setup bien hecho compra la eliminación del costo de fragilidad; no son dos males independientes, uno cura al otro.
Ejercicios
Ejercicio 1 — Explica el factor de 200x. Con tus palabras, explica de dónde sale la diferencia enorme entre el FakeBookingRepository y el SqliteBookingRepository en disco, y por qué el fake no puede "arreglarse" para ser tan realista como el real sin dejar de ser rápido.
Ver solución
La diferencia sale de la durabilidad: cada save del repositorio real hace commit(), y un commit obliga a la base de datos a garantizar que el dato quedó escrito físicamente en el disco antes de seguir. Escribir en disco y esperar la confirmación es una operación lentísima comparada con lo que hace el fake, que solo guarda una referencia en un dict de Python —mover un puntero en memoria—. Cientos de ciclos de "espera a que el disco confirme" contra cientos de "mueve un puntero" producen el factor de doscientos.
El fake no puede volverse tan realista sin perder su velocidad porque su velocidad es su falta de realismo. Lo que lo hace rápido —no tocar el disco, no serializar, no garantizar durabilidad— es exactamente lo que lo hace un modelo simplificado de lo real. Si le añadieras durabilidad de verdad (escribir a disco, hacer commit), dejaría de ser un fake en memoria: sería un repositorio real, con su costo real. No hay forma de tener las dos cosas: el fake compra velocidad pagando con realismo, y el real compra realismo pagando con velocidad. Por eso quieres los dos, cada uno en su nivel de la pirámide.
Ejercicio 2 — Aplica la regla de decisión. Para cada costura, recorre las tres preguntas de la regla y decide qué haces: (a) Calendar.is_available (lógica en memoria); (b) SqliteBookingRepository guardando y leyendo una reserva (frontera de disco); (c) el PaymentGateway real cobrando (frontera de red que cuesta dinero).
Ver solución
- (a)
Calendar.is_available— sin riesgo de costura, no pagues integración. Pregunta 1: ¿riesgo de divergencia? No: es lógica pura en memoria, no hay "pieza real distinta" que pueda comportarse diferente (elCalendarreal ya es barato y determinista). Decisión: unit test directo, cero integración. Malgastar el nivel de integración aquí no compra nada. - (b)
SqliteBookingRepositorysave/get — riesgo real, integración estrecha. Pregunta 1: ¿riesgo? Sí, frontera de disco con serialización (la trampa deldatetime, las restricciones del esquema). Pregunta 2: ¿contrato o integración? Ambos: un contrato para garantizar que el fake no diverja (módulos 3-4) y unos pocos tests de integración estrechos contra la base real para la serialización y las transacciones (módulos 5-7). Pregunta 3: lo más estrecho posible —unsave+getdirecto al repositorio, sinBookingServicede por medio—. - (c)
PaymentGatewayreal cobrando — riesgo real, pero jamás en tu suite. Pregunta 1: ¿riesgo? Sí, y encima cuesta dinero de verdad y vive en la red. Pregunta 2: aquí la integración con la pieza de verdad está vetada por el costo (cobrarías tarjetas en cada corrida); se cubre con un contrato contra el fake y, si acaso, con un test manual contra el sandbox del proveedor, fuera de la suite automática. Decisión: doblar siempre en la suite, contrato para mantener honesto al doble, y la verificación contra lo real —si existe— fuera de esta guía.
El patrón: la regla te lleva sola a doblar donde no hay riesgo o donde lo real es prohibitivo, y a integrar (estrecho) donde el riesgo de la frontera lo justifica y el costo es asumible. Eso es la pirámide, decidida costura por costura.
Ejercicio 3 — El presupuesto de la suite. Tu suite de Reservo tiene 300 unit tests que corren en 0.3s en total. Quieres añadir integración. Un compañero propone 200 tests de integración, cada uno tocando el disco. Estima el impacto en el tiempo de la suite usando el factor medido, y propón una alternativa que respete el presupuesto.
Ver solución
La estimación. Un unit test en esta suite cuesta, en promedio, 0.3s / 300 = 1 ms. Un test de integración que toca el disco cuesta, por el factor medido, del orden de 200 veces más: unos 200 ms cada uno (el mismo orden que el save+get real del ejemplo, dominado por el commit). Doscientos de esos son 200 × 200 ms = 40 s —más de cien veces lo que tarda toda la suite unitaria actual—. La suite pasaría de correr en 0.3s a tardar más de medio minuto, dominada por completo por la integración. Se correría menos, y perderías el ciclo rápido que hace útil a los unit tests.
La alternativa que respeta el presupuesto:
- No dupliques lógica en integración. La mayoría de esos 200 tests seguramente re-prueban reglas de negocio (precios, reembolsos) que ya cubren los unit tests. Esos no aportan verdad de costura; bájalos al nivel unitario, gratis.
- Quédate con un puñado de tests de integración estrechos —una docena, no doscientos— para las costuras de riesgo real: el ida-y-vuelta del repositorio,
getde id ausente, actualización sin duplicar, una transacción, una restricción del esquema. Estrechos y pocos: quizás12 × 200 ms = 2.4 s, un costo asumible. - Usa un contrato (módulos 3-4) para garantizar que el fake no diverge del real, corriéndolo contra ambos: eso te da confianza en todas las costuras dobladas sin pagar integración por cada una.
- Administra la fragilidad y el costo con bases en memoria por test donde puedas y fixtures que aíslan (módulo 7), para que esos pocos tests de integración no se vuelvan lentos ni frágiles.
Resultado: una pirámide sana —300 unitarios en 0.3s, una docena de integraciones estrechas en un par de segundos, un contrato que cubre el resto— en vez de un cono de helado de 40s. El presupuesto se respeta gastando la prueba cara solo donde su verdad vale el precio.
Resumen y siguiente paso
En esta lección le pusiste precio a la integración. Mediste, con números reales, que el SqliteBookingRepository sobre disco es del orden de doscientas veces más lento que el FakeBookingRepository en memoria, y entendiste de dónde sale —la durabilidad del commit, que es justo lo que hace útil y lento a una base de datos real—. Viste que al costo de tiempo se suman la fragilidad (más razones para fallar sin un bug) y el setup (crear y aislar recursos reales), y que esos tres costos son la fuerza que le da forma a la pirámide: no una convención, sino aritmética. Y saliste con una regla de decisión concreta —riesgo de costura, contrato contra integración, lo más estrecho posible— que, aplicada costura por costura, produce la pirámide sola.
Antes de avanzar deberías poder: explicar el factor de 200x y por qué el fake no puede ser rápido y realista a la vez; recorrer la regla de decisión sobre una costura cualquiera; y estimar el impacto de un plan de tests en el presupuesto de la suite.
Con esto cierras el "por qué" de la integración: sabes qué es, en qué proporción, dónde vive (la costura), por qué el unit test verde no basta, qué tipos hay y cuánto cuestan. Solo falta juntarlo todo con tus propias manos. La lección 8, el mini-proyecto, te pone a demostrar la brecha de principio a fin: un unit test con el FakeBookingRepository en verde, un test de integración con el SqliteBookingRepository real en rojo, y el diagnóstico de por qué el verde mentía —el módulo entero, en un entregable—.
Recursos
time.perf_counter— documentación de Python — el reloj de alta resolución con el que medimos el costo de cada proveedor en el ejemplo trabajado; la forma correcta de cronometrar código en Python.sqlite3— Control de transacciones ycommit(documentación de Python) — la referencia de por qué uncommites caro (la durabilidad) y cómo controlarlo; la raíz técnica del factor de 200x.- Martin Fowler — TestPyramid — el marco que esta lección completa: aquí ves por qué la pirámide tiene la forma que tiene, midiendo el costo que la fuerza.
- Documentación de pytest — Cómo medir la duración de los tests (
--durations) — el flag que te muestra qué tests de tu suite son los más lentos, la herramienta para vigilar que la integración no domine tu presupuesto de tiempo.