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

5. Test solitario contra sociable

Descripción

La lección 4 te dio la regla de qué doblar y qué mantener real; ahora vamos a ponerle nombre a los grados de esa decisión. Porque "cuántos vecinos de la unidad dejas reales" no es un interruptor de sí o no: es un dial, y los extremos de ese dial tienen nombres precisos que la industria usa para hablar sin ambigüedad. Un test solitario (solitary) deja real solo a la unidad bajo prueba y dobla a todos sus vecinos; es, en rigor, el unit test aislado de siempre. Un test sociable (sociable) deja reales a la unidad y a uno o más de sus colaboradores, para probar cómo se comportan juntos. Entre los dos hay un espectro, y saber en qué punto estás parado es lo que te deja elegir el nivel justo de realidad para lo que quieres probar.

Estos nombres no son adorno académico. Cuando alguien dice "escribí un test de book", la palabra no te dice si dobló todo (solitario, un unit test) o si dejó real el repositorio (sociable, una integración). Y esa diferencia lo cambia todo: qué garantiza el verde, cuánto tarda, qué bugs caza, qué costos hereda. Un vocabulario preciso disuelve discusiones enteras: en vez de pelear sobre si "eso es un unit test o de integración", dices "es un test sociable con el repositorio real y el resto doblado", y todos saben exactamente qué probaste. Esta lección te da ese vocabulario, lo aplica a Reservo con los dos extremos lado a lado, y te muestra por qué el punto más útil casi nunca es un extremo puro, sino el sociable parcial: real solo el vecino cuya junta te importa, doblado el resto.

Conexión con el módulo: esta lección nombra lo que las lecciones 2 a 4 construyeron. La 2 definió la integración por su costura; la 4 decidió qué dejar real alrededor; esta le pone las etiquetas —solitario, sociable— a esa decisión, para que puedas comunicarla y razonarla. Es también el puente hacia la recompensa: la lección 6 va a comparar un bookcancelget solitario (todo doblado, con el fake) contra el mismo flujo sociable (repositorio real), y verás que el solitario pasa mientras el sociable caza el bug. Sin los nombres de esta lección, esa comparación sería confusa; con ellos, es la demostración limpia de por qué la integración —el test sociable con la pieza real— ve lo que el aislado no.

Analogía: el ensayo de teatro

Piensa en cómo una compañía de teatro ensaya una obra. Al principio, cada actor ensaya solo: repasa sus líneas frente al espejo, un asistente le da los pies leyéndolos de un guion sin actuar, sin emoción, solo para que sepa cuándo hablar. El actor practica su papel aislado, con figurantes que apenas marcan las entradas. Eso es un ensayo solitario: el actor real, todos sus compañeros reemplazados por alguien que solo lee el pie. Funciona para memorizar líneas, pero no prueba la obra —nadie sabe todavía si la escena funciona cuando dos actores de verdad se responden—.

Más adelante viene el ensayo con el reparto: el actor real se para frente a otro actor real, y por primera vez la escena respira. Descubren cosas que el ensayo solitario jamás mostró: que una réplica llega demasiado rápido, que dos personajes se tapan al moverse, que un gesto de uno cambia el tiempo del otro. Eso es un ensayo sociable: los actores reales interactuando. Y rara vez ensayan la obra entera con las cincuenta personas, el vestuario, las luces y el público de golpe —eso es el estreno, caro y sin red—; ensayan escena por escena, dos o tres actores reales por vez, el resto marcado. Ese ensayo parcialmente sociable —los actores de la escena que importa, reales; el resto, figurantes— es donde se pule la obra. En Reservo, el actor solitario es book con todo doblado; el ensayo con el reparto es book con el repositorio real; y el ensayo parcialmente sociable —repositorio real, pago y correo marcados con dobles— es el caballo de batalla de tus integraciones.

Los dos extremos del dial

Definamos los términos con precisión, sobre la unidad bajo prueba (en Reservo, BookingService):

Test solitario (solitary). La unidad es real; todos sus colaboradores están doblados. Cero vecinos reales. Es exactamente el unit test aislado: book con FakeBookingRepository, StubPaymentGateway, SpyEmailSender y FixedClock. Verifica la lógica de orquestación de la unidad —que llama a sus colaboradores en el orden debido, con los datos correctos— suponiendo que todos se comportan como los dobles. Rápido, preciso, determinista, sin ninguna junta real bajo prueba.

Test sociable (sociable). La unidad es real y deja reales a uno o más de sus colaboradores. Al menos un vecino de verdad. book con el SqliteBookingRepository real (aunque el pago, el correo y el reloj sigan doblados) es sociable: la unidad "socializa" con un vecino real, el repositorio, y prueba cómo se comportan juntos. Es, por la definición de la lección 2, una prueba de integración —hay piezas reales a los dos lados de una costura—.

La palabra "sociable" es literal: mide si la unidad se lleva bien con sus vecinos reales. Y aquí está el matiz que más importa en la práctica: casi nunca quieres un test totalmente sociable —dejar reales el pago y el correo significa cobrar tarjetas y mandar correos, justo lo que la regla de la lección 4 manda doblar—. Lo que quieres, casi siempre, es un test parcialmente sociable: real el vecino cuya junta te interesa (el repositorio), doblados los demás para no heredar su costo y su fragilidad. Esa mezcla —un vecino real, el resto doblado— es exactamente la integración que armaste en la lección 4, y ahora tiene nombre.

Ejemplo trabajado: solitario y sociable, lado a lado

Veamos los dos extremos en código, sobre el mismo book. El primer test es solitario: BookingService real, todos sus vecinos doblados (el repositorio es un FakeBookingRepository). El segundo es sociable: el mismo book, pero con el SqliteBookingRepository real como vecino. La única diferencia entre los dos es qué pieza está enchufada en la costura del repositorio —un experimento con una sola variable—.

# tests/test_solitary_vs_sociable.py — solitario (todo doblado) vs sociable (repo real)
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)
CLOCK = datetime(2026, 3, 1, 9)


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


# SOLITARIO: la unidad real, TODOS sus vecinos doblados (incluido el repo)
def test_solitary_book_all_neighbors_doubled():
    repo = FakeBookingRepository()               # vecino doblado
    booking = build(repo).book(FOCUS, ANA, START, END)
    assert repo.get(booking.id).status == "confirmed"


# SOCIABLE: la unidad real conversa con un vecino REAL (el repo de SQLite)
def test_sociable_book_with_a_real_neighbor():
    repo = SqliteBookingRepository(sqlite3.connect(":memory:"))   # vecino real
    booking = build(repo).book(FOCUS, ANA, START, END)
    assert repo.get(booking.id).status == "confirmed"

Los dos tests comparten el helper build(repo), que arma BookingService con el pago, el correo y el reloj doblados; lo único que cambia es el repo que reciben. El solitario recibe el fake (cero vecinos reales); el sociable recibe el SQLite real (un vecino real). Corramos.

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

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

tests/test_solitary_vs_sociable.py::test_solitary_book_all_neighbors_doubled PASSED [ 50%]
tests/test_solitary_vs_sociable.py::test_sociable_book_with_a_real_neighbor PASSED [100%]

============================== 2 passed in 0.02s ===============================

Dos verdes, dos clases distintas de test. El solitario es un unit test: verifica la lógica de book con todos los vecinos doblados; su verde afirma "la orquestación es correcta suponiendo a los dobles". El sociable es una integración: verifica que book y el SqliteBookingRepository real colaboran; su verde afirma "estas dos piezas reales se entienden en esta costura". Fíjate en que ambos pasan para esta aserción —el status confirmado, que es un texto y cruza la costura sin cambiar de forma—. La diferencia entre solitario y sociable no siempre se ve en el resultado cuando todo va bien; se ve cuando algo en la costura real diverge. Ahí el solitario, ciego a lo real, sigue verde, y el sociable lo caza. Esa es exactamente la escena de la lección 6, con el datetime, en el flujo completo.

Y nota la decisión de diseño de ambos: el pago, el correo y el reloj están doblados en los dos tests. Ninguno es totalmente sociable —ninguno cobra tarjetas ni manda correos—. El "sociable" de aquí es parcial: real solo el repositorio, la junta que nos importa. Ese es el caballo de batalla, el punto del dial donde vive casi toda la integración práctica.

Por qué el sociable parcial es el punto dulce

Recorramos el dial para entender por qué el extremo parcial gana casi siempre.

Totalmente solitario (todo doblado) es rápido y preciso, pero no prueba ninguna junta real: es el unit test, indispensable en la base de la pirámide, pero no una integración. Su límite es el que motiva toda la guía: puede estar verde sobre un sistema roto, porque los dobles pueden mentir.

Totalmente sociable (todos los vecinos reales: pago, correo, reloj y base de datos, todos de verdad) prueba todas las juntas a la vez, pero hereda todos los costos: cobra dinero, manda correos, depende de sistemas externos, tarda, es flaky. Es un end-to-end, caro y fuera de la frontera de esta guía. Además, cuando falla, no sabes cuál de las muchas juntas reales se rompió.

Parcialmente sociable (real solo el vecino cuya junta importa, doblado el resto) es el punto dulce: prueba una junta real —la que te preocupa— sin heredar el costo de las demás. Es rápido (solo un recurso real, en proceso), determinista (el reloj doblado), sin efectos (el pago y el correo doblados), y cuando falla, el sospechoso es la única junta real que dejaste. Es la integración de la lección 4, ahora con su nombre técnico: un test sociable, pero parcial.

La recomendación práctica es clara: cuando integres, sé parcialmente sociable. Deja real el vecino de la costura bajo prueba, dobla los demás. Reservas el totalmente sociable (el end-to-end) para los poquísimos casos en que la pregunta es, precisamente, sobre la cadena completa con todo real —y eso, en esta guía, es territorio de la guía hermana—.

Errores comunes

Decir "unit test" o "test de integración" sin decir cuántos vecinos reales. Qué pasa: dos personas discuten si "el test de book" es unitario o de integración y no se entienden, porque una lo escribió solitario y la otra sociable. Por qué pasa: los términos gruesos esconden la variable que importa (cuántos vecinos reales). Cómo detectarlo: si una conversación sobre "el test de book" da vueltas, probablemente cada quien imagina un punto distinto del dial. Cómo corregirlo: aterriza con los nombres precisos —"es solitario, todo doblado" o "es sociable con el repositorio real, el resto doblado"—. La precisión disuelve la discusión, porque casi siempre las dos personas tienen razón sobre tests distintos.

Hacer el test totalmente sociable "para probar más". Qué pasa: alguien deja reales el pago y el correo además del repositorio, creyendo que así cubre más. Por qué pasa: más vecinos reales se siente como más cobertura. Cómo detectarlo: si tu test sociable cobra tarjetas, manda correos o tarda, te fuiste al extremo caro del dial. Cómo corregirlo: sé parcialmente sociable —real solo el vecino de la junta bajo prueba—. Las otras juntas las cubren otros tests (unit, contrato) más baratos; no necesitas dejarlas reales en este. Probar más juntas en un solo test no es una virtud: es acumular costo y ambigüedad de fallo.

Creer que el sociable reemplaza al solitario. Qué pasa: alguien, convencido de que la integración es superior, borra los unit tests solitarios y deja solo los sociables. Por qué pasa: si el sociable caza bugs que el solitario no ve, el sociable parece siempre mejor. Cómo detectarlo: si tu suite se volvió lenta y todos tus tests tocan un recurso real, invertiste la pirámide. Cómo corregirlo: el solitario y el sociable son complementarios. El solitario (unit) te da velocidad y precisión sobre la lógica —la base ancha—; el sociable (integración) te da confianza en las juntas reales —la franja del medio—. No es uno o el otro: es muchos solitarios y unos pocos sociables, cada uno en su lugar.

Ejercicios

Ejercicio 1 — Clasifica cada test. Para cada uno, di si es solitario, parcialmente sociable o totalmente sociable, y por qué: (a) book con fake, stub de pago, spy de correo y reloj fijo; (b) book con SqliteBookingRepository real, stub de pago, spy de correo y reloj fijo; (c) book con SqliteBookingRepository real, PaymentGateway real, SmtpEmailSender real y datetime.now() real; (d) cancel con SqliteBookingRepository real y el resto doblado.

Ver solución
  • (a) Solitario. Todos los vecinos doblados (fake, stub, spy, reloj fijo); cero vecinos reales. Es el unit test aislado de book.
  • (b) Parcialmente sociable. Un vecino real (el repositorio de SQLite), el resto doblado. Es la integración canónica del módulo: real la costura del repositorio, doblado lo caro. El caballo de batalla.
  • (c) Totalmente sociable. Todos los vecinos reales: base de datos, pago, correo y reloj de verdad. Es un end-to-end —cobra tarjetas, manda correos, es flaky por el reloj real y depende de servicios externos—. Caro, frágil y fuera de la frontera de esta guía.
  • (d) Parcialmente sociable. Un vecino real (el repositorio), el resto doblado, esta vez sobre el flujo de cancel. Igual que (b) pero para otra operación. Sigue siendo el punto dulce.

El patrón: cuenta los vecinos reales. Cero → solitario. Uno o algunos (el de la junta que importa) → parcialmente sociable. Todos → totalmente sociable. Y el punto dulce, casi siempre, es "algunos": el vecino real que pruebas, doblado el resto.

Ejercicio 2 — El mismo verde, dos afirmaciones. El solitario (a) y el parcialmente sociable (b) del ejercicio anterior pasan los dos en verde para assert repo.get(booking.id).status == "confirmed". Escribe la afirmación exacta que garantiza cada verde, y explica en qué escenario concreto uno seguiría verde mientras el otro se pondría rojo.

Ver solución
  • Verde del solitario (a): garantiza "la lógica de book guarda una reserva con estado confirmado, suponiendo que el repositorio se comporte como el FakeBookingRepository". Es una afirmación condicional sobre la orquestación, con el comportamiento del repositorio dado por bueno (el fake).
  • Verde del sociable (b): garantiza "BookingService y el SqliteBookingRepository real colaboran: una reserva creada por book se escribe en la base de datos de verdad y se lee de vuelta con estado confirmado". Ya no hay suposición sobre el repositorio; se ejerció el real.

El escenario donde divergen: imagina que el SqliteBookingRepository real tuviera un bug en save —por ejemplo, que guardara el status en una columna con una restricción CHECK que rechaza 'confirmed', o que serializara mal el estado—. El solitario (a) seguiría verde, porque el fake no tiene ese bug: guarda el objeto tal cual. El sociable (b) se pondría rojo, porque la costura real tropezaría con el problema al escribir o al leer. Ahí se ve la diferencia: cuando la costura real diverge del fake, el solitario es ciego y el sociable lo caza. Es, en pequeño, la escena de la lección 6.

Ejercicio 3 — Diseña la escalera de tests para cancel. Quieres cubrir el flujo de cancel con la combinación correcta de tests solitarios y sociables. Describe qué tests montarías, de solitario a sociable, y qué te da cada uno.

Ver solución

Una escalera razonable, de la base ancha hacia arriba:

  1. Solitarios (unit) de la lógica de reembolso. cancel con todos los vecinos doblados (fake, stub, spy, reloj fijo), parametrizado por las tres anclas: 72 h → 6000, 36 h → 3000, 12 h → 0. Rápidos, precisos, muchos. Verifican que la lógica de cancel calcula el reembolso correcto suponiendo a los dobles. Es la mayor parte de tu confianza sobre la lógica.
  2. Un contrato del repositorio (módulos 3-4). La batería parametrizada que corre las cláusulas del repositorio contra el fake y contra SQLite. Garantiza que el fake no miente sobre lo que el contrato cubre. No es solitario ni sociable de cancel: es la certificación de la pieza vecina.
  3. Uno o dos parcialmente sociables de cancel. cancel con el SqliteBookingRepository real y el resto doblado, para verificar que el flujo completo —leer la reserva del real, recalcular, guardar cancelada, releer— funciona con la base de datos de verdad. Pocos, porque son más caros. Aquí es donde, en la lección 6, saldrá el TypeError del datetime si el repositorio no convierte de vuelta.

La forma es la pirámide: muchos solitarios (lógica), un contrato (el vecino certificado), unos pocos sociables (la junta real). Cada escalón cubre una clase de fallo distinta —la lógica, la divergencia del doble, la colaboración real— y ninguno reemplaza a los otros. Esa es la suite madura para cancel.

Resumen y siguiente paso

En esta lección le pusiste nombre a los grados de la decisión de la lección 4. Un test solitario deja real solo a la unidad y dobla a todos sus vecinos —el unit test aislado—; un test sociable deja reales a uno o más vecinos —una integración—. Con el ensayo de teatro entendiste la diferencia entre el actor practicando solo con figurantes (solitario) y la escena con el reparto real (sociable), y por qué el ensayo escena por escena, con los actores que importan reales y el resto marcado, es donde se pule la obra. Viste book solitario y book sociable lado a lado, ambos verdes, y entendiste que la diferencia se revela cuando la costura real diverge. Y ubicaste el punto dulce: el sociable parcial —real el vecino cuya junta importa, doblado el resto—, el caballo de batalla de la integración práctica.

Antes de avanzar deberías poder: clasificar cualquier test como solitario, parcialmente sociable o totalmente sociable contando sus vecinos reales; traducir el verde de un solitario (condicional) y el de un sociable (sobre lo real) y decir en qué escenario divergen; y diseñar una escalera de tests que combine solitarios, contrato y sociables sin que uno reemplace a los otros.

Llegamos al clímax del módulo. Hasta aquí, todos los tests sociables que corrimos pasaron en verde —dejamos las aserciones dentro de lo que cruza la costura sin problema—. En la lección 6 vamos a llevar el flujo completo bookcancelget contra el repositorio real y a ver cómo la integración caza lo que ni el unit ni el contrato aislado ven: el datetime que volvió como str y que cancel no puede restar, un TypeError en el flujo vivo. El solitario pasa; el sociable explota; y ahí, por fin, cobras todo lo que la integración te devuelve.

Recursos

  • Martin Fowler — UnitTest (solitary vs sociable) — la fuente que acuña "solitario" y "sociable" y discute las dos escuelas que prefieren cada uno; el marco exacto de esta lección.
  • Martin Fowler — IntegrationTest — el complemento sobre por qué un test sociable con una pieza real es, por definición, una integración, y cómo se relaciona con el alcance (estrecho/amplio).
  • Documentación de pytest — Parametrizar tests — la referencia para la escalera de tests del ejercicio 3, donde los solitarios de las tres anclas de cancel se escriben como un solo test parametrizado.
  • test-doubles-and-test-data-guide — la guía hermana de los dobles que un test solitario usa para todos sus vecinos y un sociable parcial para todos menos uno; útil para recordar qué doble corresponde a cada colaborador cuando armas cualquier punto del dial.