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

2. Integración contra unidad

Descripción

En la lección anterior viste la brecha: el mismo book que pasa con un doble falla con la pieza real. Para entender por qué —y qué hacer al respecto— necesitas dos palabras con un significado exacto, no borroso: test unitario y test de integración. Mucha gente las usa como sinónimos vagos de "test chico" y "test grande", y esa vaguedad es justamente lo que deja pasar el bug de la lección 1. Esta lección les pone un filo preciso.

La definición que vamos a usar es de aislamiento, no de tamaño. Un test unitario prueba una unidad aislada de sus colaboradores reales: donde la unidad tocaría una base de datos, una pasarela de pago o un servidor de correo, tú pones un doble. La unidad corre su lógica; los dobles rellenan el resto; nada real cruza la costura. Un test de integración prueba dos o más piezas reales trabajando juntas, cruzando la costura que las une: BookingService y el SqliteBookingRepository de verdad, con la reserva viajando de uno al otro y de vuelta. La diferencia no está en cuántas líneas de código tenga el test ni en cuánto tarde: está en si hay una pieza real en la costura. Con solo dobles, es unitario. Con al menos una pieza real cruzando su junta, es integración.

Conexión con el módulo: esta lección es el cimiento de vocabulario sobre el que se paran las otras siete. La lección 1 te mostró la brecha; esta te da los nombres para hablar de ella sin ambigüedad. La 3 usará estos nombres para armar la pirámide (cuántos de cada uno), la 4 para ubicar las costuras (dónde vive cada tipo), y la 5 para explicar con precisión por qué el unitario verde no vio lo que el de integración sí. Sin estas dos definiciones firmes, el resto de la guía sería una conversación de sordos.

Analogía: el examen de una sola materia y el examen integrador

Piensa en cómo te evaluaban en la escuela. Había dos clases de examen. El examen de una materia medía una cosa aislada: el de álgebra solo pedía álgebra, con una calculadora permitida para que la aritmética no se metiera en el camino. Si te equivocabas, sabías exactamente qué habías fallado: álgebra, no otra cosa. La calculadora era tu "doble": sustituía una capacidad (sumar) para que el examen midiera solo la que le importaba (despejar). Y había el examen integrador de fin de año: un problema de física que exigía plantear la ecuación (álgebra), resolverla (aritmética) y aplicar la ley física correcta, todo junto, sin calculadora. Ese examen no medía una capacidad: medía si las capacidades encajaban cuando trabajaban en cadena. Podías sacar diez en álgebra, diez en aritmética y diez en física por separado, y aun así tropezar en el integrador porque, al encadenarlas, arrastrabas un signo de una a la otra.

Un unit test es el examen de una materia: aísla la unidad, dobla lo demás, y cuando falla sabes exactamente qué pieza falló. Un test de integración es el examen integrador: conecta las piezas reales y mide si encajan en cadena. Y la moraleja escolar es la misma de esta guía: sacar diez en cada materia por separado no garantiza sacar diez en el integrador, porque el integrador prueba algo que ningún examen aislado prueba —la unión—. Necesitas los dos. El examen de una materia te dice, con precisión quirúrgica, dónde estás flojo; el integrador te dice si, juntas, tus piezas resuelven el problema de verdad.

Las dos definiciones, lado a lado

Pongámoslas frente a frente, sin adornos.

Test unitarioTest de integración
Qué pruebaUna unidad aislada de sus colaboradoresDos o más piezas reales, juntas, cruzando su costura
La costuraCerrada con un doble (fake, stub, spy, mock)Abierta: la pieza real cruza de verdad
La pregunta"¿Mi lógica es correcta, suponiendo que los colaboradores cumplen?""¿Las piezas de verdad cumplen y encajan entre sí?"
Si fallaEl bug está en esta unidad (aislada, fácil de ubicar)El bug está en alguna pieza o en su junta (hay que localizar)
VelocidadMicrosegundos: todo en memoriaMás lento: toca disco, red, procesos
DeterminismoAlto: los dobles no tienen clima propioMenor: depende del estado real (archivos, conexiones)

La fila que más importa es la pregunta, porque de ella se derivan todas las demás. El unit test pregunta si tu lógica es correcta dando por sentado que los colaboradores se portan como supones. El test de integración pregunta si esa suposición era cierta y si, cuando las piezas se conectan de verdad, encajan. Son preguntas distintas, y por eso una suite sana responde las dos: la primera te da velocidad y precisión al depurar; la segunda te da confianza en que el sistema ensamblado funciona.

Fíjate en algo que la tabla deja claro y que la intuición suele borrar: la velocidad es una consecuencia, no la definición. Un unit test es rápido porque está aislado (los dobles viven en memoria), no al revés. Si llamas "unit test" a algo solo porque es rápido, pero por dentro abre una conexión a SQLite, no es un unit test rápido: es un test de integración que resultó rápido, con toda la fragilidad de la integración escondida bajo una etiqueta equivocada. El día que ese archivo de base de datos se bloquee, tu "unit test" fallará por una razón que ningún unit test debería tener.

Ejemplo trabajado: el mismo book, dos preguntas

Nada aclara la distinción como verla sobre el mismo código. Aquí está book de Reservo probado dos veces. El primer test es unitario: BookingService es la unidad, y sus cuatro colaboradores están todos doblados. Pregunta si book orquesta bien —si cobra el monto correcto, guarda, y confirma por correo—, dando por sentado que guardar funciona. El segundo es de integración: BookingService se conecta al SqliteBookingRepository real. Pregunta algo que el primero no puede: si lo que book guarda queda de verdad en la base de datos y se puede leer.

# tests/test_unit_vs_integration.py
import sqlite3
from datetime import datetime

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")
START = datetime(2026, 3, 10, 9)
END = datetime(2026, 3, 10, 12)          # Focus 3 h
CLOCK = datetime(2026, 3, 1, 9)


# UNIT — BookingService aislado, el repo es un doble.
# Pregunta: "¿book orquesta bien (cobra, guarda, confirma)?"
def test_book_orchestrates_correctly_unit():
    payments = StubPaymentGateway(ok=True)
    emails = SpyEmailSender()
    repo = FakeBookingRepository()
    service = BookingService(Calendar(), FixedClock(CLOCK), payments, emails, repo)

    booking = service.book(FOCUS, ANA, START, END)

    assert booking.price_cents == 6000            # cobro correcto
    assert payments.charges == [(6000, "m-ana")]  # cobro una vez
    assert len(emails.sent) == 1                  # confirmo por correo
    assert booking.status == "confirmed"


# INTEGRATION — BookingService + SqliteBookingRepository real.
# Pregunta: "¿lo que book guarda queda de verdad en la base de datos, legible?"
def test_book_persists_to_real_db_integration():
    repo = SqliteBookingRepository(sqlite3.connect(":memory:"))
    service = BookingService(Calendar(), FixedClock(CLOCK),
                             StubPaymentGateway(ok=True), SpyEmailSender(), repo)

    booking = service.book(FOCUS, ANA, START, END)

    saved = repo.get(booking.id)
    assert saved.price_cents == 6000       # el entero cruzo la costura
    assert saved.status == "confirmed"     # el texto cruzo la costura
    assert saved.room_id == "focus"

Mira las diferencias, porque son las de la tabla, hechas código. El unit test dobla el repo (FakeBookingRepository) y verifica cosas sobre el objeto que book devuelve y sobre los dobles (payments.charges, emails.sent): la lógica de orquestación. El test de integración usa el SqliteBookingRepository real, y hace algo que el unit test no puede: después de reservar, vuelve a leer de la base de datos (repo.get) y verifica que la reserva quedó guardada y legible. La costura —entre BookingService y la persistencia— está doblada en el primero y abierta en el segundo.

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

python3 -m pytest tests/test_unit_vs_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_unit_vs_integration.py::test_book_orchestrates_correctly_unit PASSED [ 50%]
tests/test_unit_vs_integration.py::test_book_persists_to_real_db_integration PASSED [100%]

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

Los dos en verde. Y esto es importante para no caricaturizar: un test de integración no es "el que siempre falla". Aquí el de integración pasa, porque las aserciones que hace —sobre price_cents (un entero), status y room_id (textos)— caen sobre campos que cruzan la costura sin cambiar de forma. El de integración solo se pone rojo cuando la costura tiene una divergencia real, como la del datetime de la lección 1. La integración no es sospecha permanente; es la única prueba que puede confirmar que las piezas encajan, y a veces la respuesta es "sí encajan", en verde. Lo que la hace valiosa es que puede dar cualquiera de las dos respuestas con la verdad, mientras el unit test solo puede responder por la mitad de la costura.

Por qué necesitas los dos (y no puedes elegir uno)

Es tentador buscar un ganador. "Si la integración prueba las piezas reales, ¿para qué los unit tests?" O al revés: "si los unit tests son rápidos y precisos, ¿para qué la lentitud de la integración?" Las dos preguntas tienen la misma respuesta: porque cada uno responde algo que el otro no puede.

Lo que solo el unit test te da: precisión al fallar. Cuando test_book_orchestrates_correctly_unit falla, sabes que el bug está en la lógica de BookingService, porque todo lo demás está doblado —no hay base de datos que pueda estar rota, ni red que pueda estar caída—. El fallo apunta a un solo lugar. Además, es tan rápido que puedes correr miles en el tiempo que un test de integración tarda en abrir una conexión. Esa velocidad no es un lujo: es lo que te deja correr la suite en cada guardado, mantener el ciclo rojo-verde-refactor apretado, y recibir el fallo mientras todavía tienes el cambio fresco en la cabeza.

Lo que solo la integración te da: confianza en las juntas. El unit test da por sentado que el repositorio cumple lo que BookingService espera. La integración verifica esa suposición. La lección 1 mostró qué pasa cuando la suposición es falsa: 200 unit tests verdes y una pantalla rota en producción. Ninguna cantidad de unit tests cubre una junta que ningún unit test toca. La integración es el único test que puede decir "sí, la pieza real de verdad se comporta como tu lógica supone" —o "no, no lo hace, y aquí está la línea"—.

Quitar cualquiera de los dos te deja ciego de un ojo. Solo unit tests: rápido y preciso, pero sin ver las juntas —el modo en que la lección 1 se rompió—. Solo integración: ves las juntas, pero cada fallo es una investigación (¿fue mi lógica, la base de datos, la red?), la suite tarda minutos y falla por el clima de la infraestructura. La respuesta no es elegir: es combinar en la proporción correcta, y esa proporción tiene una forma —la pirámide— que es el tema de la lección 3.

Errores comunes

Definir "unit test" por la velocidad en vez de por el aislamiento. Qué pasa: alguien mide "¿tarda menos de X ms?" y, si sí, lo llama unit test, sin importar qué toca por dentro. Por qué pasa: la velocidad es medible y visible; el aislamiento hay que razonarlo. Cómo detectarlo: abre el test y busca si alguna pieza real cruza una costura —una conexión a SQLite, un archivo, un socket—. Si la hay, es integración, tarde lo que tarde. Cómo corregirlo: clasifica por aislamiento. "¿Hay una pieza real en la costura?" Si no, unitario; si sí, integración. La velocidad es la consecuencia, no el criterio.

Creer que un test de integración "prueba más" y por eso es mejor. Qué pasa: alguien concluye que como la integración toca lo real, es una prueba superior, y prefiere escribir siempre integración. Por qué pasa: "más real" se siente como "más verdadero". Cómo detectarlo: si tu suite tarda minutos, es difícil saber qué falló cuando falla, y a veces se pone roja sin que cambiaras código, te pasaste de integración. Cómo corregirlo: no prueba más, prueba otra cosa —las juntas, no la lógica aislada—. Un fallo de integración es ambiguo por diseño (puede ser cualquiera de las piezas o su junta); un fallo unitario es preciso. Se complementan; ninguno es "mejor".

Doblar una pieza en un test que se anuncia como de integración. Qué pasa: alguien escribe un "test de integración de book" pero dobla el repositorio con el fake para que sea más fácil. Por qué pasa: el fake es cómodo y el real pide setup. Cómo detectarlo: si la pieza que dice integrar está doblada, la costura que importaba sigue cerrada, y el test es unitario disfrazado. Cómo corregirlo: un test de integración debe dejar real justamente la pieza cuya junta quieres verificar. Puedes doblar otras (el pago, el correo) para acotar el ruido —eso es legítimo y lo veremos en la lección 6—, pero la costura bajo prueba tiene que estar abierta, o no estás integrando nada.

Ejercicios

Ejercicio 1 — Clasifica y justifica. Para cada test, di si es unitario o de integración, y nombra la costura que está cerrada o abierta: (a) refund_cents(booking, 6000, now) verificado directo para 72/36/12 h; (b) cancel con FakeBookingRepository, StubPaymentGateway y SpyEmailSender; (c) find_by_room del SqliteBookingRepository real devuelve las dos reservas de la sala Focus; (d) book con SqliteBookingRepository real y StubPaymentGateway doblado.

Ver solución
  • (a) refund_cents directo — unitario. No hay colaboradores; es lógica pura. No hay ninguna costura que abrir: la unidad no habla con nadie. El unit test más limpio posible.
  • (b) cancel con tres dobles — unitario. La costura entre BookingService y cada colaborador (repo, pagos, correo) está cerrada con un doble. Se prueba la lógica de orquestación de cancel aislada.
  • (c) find_by_room del repo real — integración. La costura entre SqliteBookingRepository y su base de datos está abierta: se prueba el componente real contra su recurso real. Que solo intervenga una pieza no lo hace unitario; lo que cuenta es que la pieza es real y cruza su junta.
  • (d) book con repo real y pago doblado — integración. La costura clave —BookingService ↔ persistencia— está abierta (repo real), aunque la costura del pago esté cerrada (stub). Basta con que una costura relevante esté abierta con una pieza real para que sea integración. Doblar el pago solo acota el ruido; no lo convierte en unitario.

La regla: mira las costuras del test. Si todas están cerradas con dobles, unitario. Si al menos una está abierta con una pieza real, integración —sin importar cuántas piezas haya ni cuánto tarde—.

Ejercicio 2 — La pregunta que responde cada uno. Reservo tiene un bug: SqliteBookingRepository.save olvida la coma en el SQL y no guarda el status, así que toda reserva se lee como status=None. ¿Cuál de los dos tests de esta lección lo habría atrapado, y por qué el otro no?

Ver solución

Lo habría atrapado el test de integración (test_book_persists_to_real_db_integration), y en concreto la aserción assert saved.status == "confirmed". Ese test lee la reserva de vuelta desde la base de datos real, así que si save no guardó bien el status, saved.status sería None y la aserción fallaría, señalando el bug.

El unit test (test_book_orchestrates_correctly_unit) no lo habría atrapado, porque usa el FakeBookingRepository, cuyo save guarda el objeto entero en un dict sin ejecutar una sola línea de SQL. El bug vive exclusivamente en el SQL de SqliteBookingRepository.save, una pieza que el unit test nunca toca. Su pregunta —"¿book orquesta bien?"— se responde igual con o sin el bug, porque el bug está debajo de la costura que el unit test cierra con el fake.

Esto es la lección en una frase: cada test responde su pregunta y solo la suya. La del unit test es "¿mi lógica de orquestación es correcta?"; la de integración es "¿la pieza real cumple y persiste de verdad?". Un bug de SQL vive en el territorio de la segunda pregunta, así que solo el test que hace esa pregunta lo ve.

Ejercicio 3 — El test de integración que pasó en verde. En el ejemplo trabajado, el test de integración pasó. Un compañero dice: "entonces no sirvió de nada, no encontró ningún bug". Explica por qué un test de integración en verde sí aporta valor, y qué habría significado que fallara.

Ver solución

Un test de integración en verde aporta confianza confirmada, no una simple ausencia de noticias. Antes de correrlo, "que book persiste bien en SQLite" era una suposición: creíamos que el price_cents, el status y el room_id cruzaban la costura intactos, pero no lo habíamos verificado contra la pieza real. El test en verde convierte esa suposición en un hecho comprobado: sí, esos campos se guardan y se leen correctamente desde la base de datos de verdad. Es exactamente el valor del examen integrador que sale bien: no "no aprendí nada", sino "confirmé que mis piezas encajan en cadena".

Si el test hubiera fallado, habría significado que una de esas suposiciones era falsa —que algún campo no cruza la costura como creíamos, como pasó con el datetime de la lección 1—. Y lo habría dicho antes de producción, con la línea exacta. Que un test pueda dar cualquiera de las dos respuestas con la verdad es lo que lo hace útil: un test que solo puede pasar no prueba nada, pero este podía haber fallado (lo vimos fallar en la lección 1 con otra aserción) y no lo hizo. El verde, aquí, es información: la costura, para estos campos, está sana.

Resumen y siguiente paso

En esta lección le pusiste filo a las dos palabras que sostienen la guía. Un test unitario prueba una unidad aislada de sus colaboradores, con la costura cerrada por dobles, y responde "¿mi lógica es correcta, suponiendo que los colaboradores cumplen?". Un test de integración prueba dos o más piezas reales juntas, con la costura abierta, y responde "¿las piezas de verdad cumplen y encajan?". La distinción es de aislamiento, no de tamaño ni de velocidad; la velocidad es una consecuencia. Y viste, con el mismo book probado dos veces, que necesitas los dos: el unitario por su precisión y su velocidad, la integración por la confianza en las juntas que ningún unitario puede dar.

Antes de avanzar deberías poder: clasificar cualquier test como unitario o de integración mirando si hay una pieza real en la costura; enunciar la pregunta que responde cada uno; y explicar por qué quitar cualquiera de los dos te deja ciego de un ojo.

Ahora que sabes que quieres los dos, la pregunta inmediata es en qué proporción. ¿Cuántos unitarios, cuántos de integración? La respuesta tiene una forma célebre —una pirámide— y una lógica detrás que explica por qué esa forma, y no otra, es la que mantiene una suite rápida, confiable y barata de mantener. Es la lección 3.

Recursos

  • Martin Fowler — UnitTest — el ensayo que discute por qué "unit test" es un término más resbaladizo de lo que parece y por qué el aislamiento (no el tamaño) es el criterio útil; el trasfondo de la definición que usamos en esta lección.
  • Martin Fowler — IntegrationTest — la contraparte: qué significa realmente probar la integración, y la distinción entre integración estrecha y amplia que retomaremos en la lección 6.
  • Documentación de pytest — Cómo invocar pytest — la referencia para correr un archivo o un test concreto (-v, seleccionar por nombre), que usamos para ejecutar y separar el unitario del de integración.
  • sqlite3 — DB-API para SQLite (documentación de Python) — la referencia del módulo real que hace de "pieza de verdad" cada vez que abrimos una costura de integración en la guía.