Módulo 2: Entender y fijar el legacy

Encontrar el seam (la costura)

Descripción

En la lección 3 caracterizamos una función pura: le das un ítem, devuelve un precio, y el precio solo depende del ítem. Ese es el caso amable. El caso real casi nunca es tan limpio, porque el legacy está soldado a su entorno: consulta el reloj del sistema, lee una variable global, golpea la base de datos, llama a un servicio externo. Y en cuanto una función depende de algo así, se vuelve imposible de fijar de forma determinista: su salida cambia según el día en que la corras, según el valor de una global que otro proceso modifica, o según lo que responda una BD que no controlas. No puedes tomar una foto de algo que se mueve solo. Esta lección resuelve ese problema con una idea que Michael Feathers convirtió en herramienta central del oficio: el seam.

Un seam —una costura— es, en palabras de Feathers, un lugar donde puedes alterar el comportamiento de tu programa sin editar en ese lugar. Es el punto donde puedes meter la mano —insertar una prueba, sustituir una dependencia, desviar una llamada— sin abrir en canal la función. La imagen es la de una prenda: no descoses toda la camisa para cambiar un botón; usas la costura, que está hecha para abrirse y cerrarse. En el software, encontrar el seam significa localizar el punto exacto donde la dependencia problemática entra a la función, y convertir ese punto en algo que el test pueda controlar —darle al reloj, a la global o a la BD un valor que tú decides—, sin reescribir la lógica que quieres caracterizar. Con el seam abierto, la función vuelve a ser determinista, y la puedes fijar.

Conexión con el módulo. La lección 3 te enseñó a fijar comportamiento cuando puedes correr la función de forma repetible; esta te enseña qué hacer cuando no puedes, que es el caso más frecuente en un legacy real. El seam es lo que hace aplicable el characterization test al código soldado a su entorno: sin él, la técnica de la lección 3 se queda en la teoría. La 5 después escala la caracterización a cientos de casos, y la 6 enfrenta las rarezas. La frontera con el ecosistema de Testing se mantiene: aquí no vamos a la teoría de dobles de prueba, mocks y stubs a fondo —eso es Testing—; usamos el seam como técnica de migración para poder fijar el legacy antes de tocarlo. Y ojo con la frontera interna de la guía: abrir un seam para insertar una prueba es esto; abrir una capa de abstracción para migrar la implementación por dentro (con dos versiones coexistiendo tras un flag) es branch by abstraction, el módulo 4. Se parecen —ambos insertan un punto de control— pero su propósito es distinto: aquí, testear; allá, reemplazar.

Una analogía: la llave de paso y la junta del plomero

Vuelve al baño en renovación. Necesitas cambiar la regadera, que está conectada a la tubería de agua. Hay dos formas de hacerlo. La forma bárbara: cortar el tubo con una sierra, arrancar la regadera, y soldar una nueva —invasiva, irreversible, y con toda el agua de la casa cerrada porque no hay dónde detenerla localmente—. La forma del plomero con oficio: buscar la junta —esa unión roscada que conecta la regadera al tubo— y desenroscar por ahí. La junta existe precisamente para eso: es el punto diseñado para conectar y desconectar sin tocar el resto de la instalación. Y si además hay una llave de paso en ese ramal, puedes cerrar el agua solo de ahí, cambiar la regadera con calma, y volver a abrir, sin que el resto de la casa se entere.

El seam es la junta y la llave de paso del código. La función de precio está "conectada" al reloj del sistema: pregunta qué día es hoy para decidir si el cupón venció. Si quieres testearla, la forma bárbara sería reescribirla para que no consulte el reloj —invasiva y arriesgada—. La forma con oficio es encontrar el punto donde entra la dependencia del reloj y convertirlo en una junta: hacer que la fecha entre por un parámetro en vez de que la función la vaya a buscar sola. Con esa junta, el test puede "cerrar la llave de paso" —fijar la fecha a un valor que él decide—, correr la función con calma en condiciones controladas, y obtener siempre el mismo resultado. La lógica del precio no se tocó; solo abrimos la costura por donde entraba el reloj. Igual que el plomero: no cortaste el tubo, usaste la junta.

Lo bonito de una junta bien hecha es que el que usa la regadera no nota nada: abre la llave y sale agua, igual que antes. Un buen seam tiene esa misma propiedad —los llamadores viejos de la función siguen usándola idéntico, sin enterarse de que ahora hay una junta—. Eso es lo que la hace segura de introducir, y es la clave del ejemplo que sigue.

Ejemplo trabajado: el seam del reloj

La función de precio tiene un cupón con fecha de vencimiento: el descuento solo aplica si el cupón no venció, y para saberlo la función consulta date.today() —el reloj real del sistema—. Eso la hace no determinista: el mismo ítem da un precio distinto según el día en que corras el test. Vamos a ver el problema y a abrir el seam.

import math
from datetime import date

TAX_RATE = 0.16
CATEGORY_DISCOUNT = {"electronics": 0.05, "books": 0.10, "toys": 0.08, "grocery": 0.00}


def _floor_cents(a):
    return math.floor(a * 100) / 100


# ---------------------------------------------------------------------------
# ANTES: la dependencia esta soldada por dentro. Imposible de fijar.
# ---------------------------------------------------------------------------
def legacy_price_welded(item):
    price = _floor_cents(item["unit_price"] * item["quantity"])
    price = _floor_cents(price * (1 - CATEGORY_DISCOUNT.get(item["category"], 0.0)))
    coupon = item.get("coupon_percent", 0.0)
    # El cupon solo aplica si no ha vencido... y pregunta la fecha REAL de hoy.
    if coupon and item.get("coupon_expires") and date.today() <= item["coupon_expires"]:
        price = _floor_cents(price * (1 - coupon))
    return _floor_cents(price * (1 + TAX_RATE))


# ---------------------------------------------------------------------------
# DESPUES: mismo comportamiento, pero con un SEAM. 'today' entra como
# parametro con valor por defecto: los llamadores viejos NO se enteran,
# y el test controla la fecha. (Seam de parametrizacion, estilo Feathers.)
# ---------------------------------------------------------------------------
def legacy_price_seamed(item, today=None):
    if today is None:                 # enabling point: default = comportamiento viejo
        today = date.today()
    price = _floor_cents(item["unit_price"] * item["quantity"])
    price = _floor_cents(price * (1 - CATEGORY_DISCOUNT.get(item["category"], 0.0)))
    coupon = item.get("coupon_percent", 0.0)
    if coupon and item.get("coupon_expires") and today <= item["coupon_expires"]:
        price = _floor_cents(price * (1 - coupon))
    return _floor_cents(price * (1 + TAX_RATE))


ITEM = {
    "sku": "P-9", "unit_price": 50.00, "quantity": 2, "category": "books",
    "coupon_percent": 0.20, "coupon_expires": date(2026, 6, 30),
}

print("=== ANTES del seam: el test depende de que dia lo corras ===")
print("El cupon vence el 2026-06-30. El resultado del test cambia con el reloj:")
print(f"  hoy real ({date.today()}): legacy_price_welded = {legacy_price_welded(ITEM)}")
print("  Manana, o en un runner en otra zona horaria, este numero puede cambiar.")
print("  No podemos fijar el comportamiento: la funcion consulta el reloj del mundo.\n")

print("=== DESPUES del seam: el test controla la fecha, deterministico ===")
before_expiry = date(2026, 6, 15)   # cupon vigente
after_expiry = date(2026, 7, 15)    # cupon vencido

pinned_valid = legacy_price_seamed(ITEM, today=before_expiry)
pinned_expired = legacy_price_seamed(ITEM, today=after_expiry)
print(f"  cupon vigente  (today={before_expiry}): {pinned_valid}")
print(f"  cupon vencido  (today={after_expiry}): {pinned_expired}")
print()

print("characterization_test sobre el seam (fechas fijas, sin reloj real):")
cases = [
    ("cupon vigente", before_expiry, pinned_valid),
    ("cupon vencido", after_expiry, pinned_expired),
]
fails = 0
for label, when, expected in cases:
    actual = legacy_price_seamed(ITEM, today=when)
    ok = actual == expected
    fails += 0 if ok else 1
    print(f"  {'PASS' if ok else 'FAIL'}  {label:<14} today={when}  "
          f"esperado={expected}  actual={actual}")
print(f"  -> {'VERDE' if fails == 0 else 'ROJO'}: el mismo test da el mismo "
      f"resultado siempre, corras hoy o en un anio.\n")

print("El seam NO cambio el comportamiento: para el llamador viejo")
print("(sin pasar 'today') la funcion se comporta identico a antes:")
print(f"  legacy_price_welded(ITEM) = {legacy_price_welded(ITEM)}")
print(f"  legacy_price_seamed(ITEM) = {legacy_price_seamed(ITEM)}  (mismo default)")

Qué esperar. Al correr el archivo, la salida es esta (la línea del "hoy real" muestra la fecha en que se ejecutó; ese es justo el problema):

=== ANTES del seam: el test depende de que dia lo corras ===
El cupon vence el 2026-06-30. El resultado del test cambia con el reloj:
  hoy real (2026-07-29): legacy_price_welded = 104.4
  Manana, o en un runner en otra zona horaria, este numero puede cambiar.
  No podemos fijar el comportamiento: la funcion consulta el reloj del mundo.

=== DESPUES del seam: el test controla la fecha, deterministico ===
  cupon vigente  (today=2026-06-15): 83.52
  cupon vencido  (today=2026-07-15): 104.4

characterization_test sobre el seam (fechas fijas, sin reloj real):
  PASS  cupon vigente  today=2026-06-15  esperado=83.52  actual=83.52
  PASS  cupon vencido  today=2026-07-15  esperado=104.4  actual=104.4
  -> VERDE: el mismo test da el mismo resultado siempre, corras hoy o en un anio.

El seam NO cambio el comportamiento: para el llamador viejo
(sin pasar 'today') la funcion se comporta identico a antes:
  legacy_price_welded(ITEM) = 104.4
  legacy_price_seamed(ITEM) = 104.4  (mismo default)

Lee la primera sección con cuidado, porque el problema es sutil. El cupón del ítem vence el 30 de junio de 2026. Cuando se corrió este ejemplo, "hoy" era el 29 de julio de 2026 —o sea, el cupón ya venció—, así que el descuento del 20% no se aplicó y el precio salió 104.4. Pero fíjate en el problema que eso revela: ese número depende del día en que corras el código. Si lo corres en junio, el cupón está vigente, se aplica el descuento, y el precio es otro. Si lo corres en agosto, venció, y es 104.4. El mismo ítem, distintos precios, según el reloj del mundo. Ahora intenta escribir un characterization test sobre esto: ¿qué valor pones como esperado? Cualquiera que pongas será correcto solo algunos días. No puedes fijar el comportamiento de algo que se mueve solo con el calendario. La foto sale movida.

La segunda sección abre el seam y resuelve el problema. La función legacy_price_seamed es idéntica a la anterior salvo por una cosa: recibe today como parámetro. Y fíjate en el detalle que la hace segura, la línea if today is None: today = date.today() —lo que Feathers llama el enabling point, el punto de habilitación—: si nadie pasa today, la función usa el reloj real, exactamente como antes. La costura está ahí, pero cerrada por defecto. Ahora el test puede abrirla: pasa today=date(2026, 6, 15) (cupón vigente) y obtiene siempre 83.52; pasa today=date(2026, 7, 15) (vencido) y obtiene siempre 104.4. Dos casos deterministas, fijables, que no dependen del calendario. El characterization test de la tercera sección los corre y sale verde —y seguirá verde mañana, la semana que viene y dentro de un año, porque las fechas están fijas—. La foto ya no sale movida: el test controla el reloj.

Y la última sección es la que hace que valga la pena. Mira los dos números finales: legacy_price_welded(ITEM) y legacy_price_seamed(ITEM) (sin pasar today) dan exactamente lo mismo: 104.4. Esto es crucial. El seam no cambió el comportamiento de la función para quien ya la usaba. Todo el código de Mercado que llama al precio sin preocuparse por fechas sigue funcionando idéntico —el default reproduce la llamada al reloj—. Solo el test, que sí pasa today, ejerce el control nuevo. Introdujimos un punto de prueba con riesgo casi nulo: agregamos un parámetro opcional, no reescribimos ninguna lógica. Esa es la marca de un buen seam —invisible para los llamadores, poderoso para el test—.

Profundización: tipos de seam y el enabling point

Feathers distingue varios tipos de seam según por dónde abres la costura. Vale la pena conocerlos porque el legacy te va a presentar dependencias de todas las formas, y cada una se abre distinto. No es teoría de testing: es un catálogo de puntos de entrada.

  • Seam de parámetro (el que usamos). La dependencia entra como argumento. Antes la función iba a buscar date.today(); ahora la fecha entra por un parámetro con default. Es el seam más simple y de menor riesgo cuando la dependencia es un valor (una fecha, una tasa, una configuración). El default —el enabling point— preserva el comportamiento viejo.
  • Seam de objeto. Cuando la dependencia es todo un colaborador (la base de datos, un servicio de pagos, un cliente HTTP), no pasas un valor sino un objeto que la función usa. Antes la función creaba su propia conexión a la BD por dentro; con el seam, recibe el "repositorio" como parámetro, y el test le pasa uno de mentira que devuelve datos fijos. Así fijas comportamiento sin tocar la BD real.
  • Seam de "todo por defecto". El patrón general detrás de los dos anteriores: la dependencia entra como parámetro con un valor por defecto que reproduce lo que la función hacía sola. Los llamadores viejos no pasan nada y no notan el cambio; el test pasa su versión controlada. Es la forma canónica de abrir una costura en código legacy sin romper a nadie.

El corazón de todos es el enabling point: el lugar donde eliges qué implementación se usa. En nuestro ejemplo es la línea if today is None: today = date.today(). Su virtud es doble: por un lado habilita el control desde el test (puedes pasar cualquier fecha); por otro, preserva el comportamiento por defecto (si no pasas nada, es el reloj real de siempre). Un seam sin un buen enabling point es peligroso, porque obliga a cambiar a todos los llamadores; un seam con enabling point por default es seguro, porque nadie tiene que cambiar salvo el test.

Una advertencia importante sobre el orden de las cosas. Abrir un seam es, técnicamente, cambiar el código —agregas un parámetro—. Y en la lección 2 dijimos "no cambies antes de fijar". ¿No es contradictorio? No, y entender por qué es clave. El cambio que introduce un seam es de una clase especial: es preservador de comportamiento por construcción —el default garantiza que la salida no cambia para ningún llamador existente—. Es el "mínimo cambio riesgoso posible" del que hablaba la lección 2: el cambio justo, y verificable, que te permite empezar a fijar lo que antes no podías. El ejemplo lo demostró midiendo que welded y seamed dan lo mismo. Aun así, la prudencia manda hacerlo pequeño, revisable y —cuando se pueda— con la parte determinista de la función ya fijada, para reducir todavía más el riesgo. Primero fijas lo que puedes sin tocar nada; abres seams solo donde una dependencia lo exija; y ahí sí terminas de fijar.

flowchart LR
    subgraph Antes["ANTES: dependencia soldada"]
      F1["price(item)"] --> C1["date.today()<br/>(reloj del mundo)"]
    end
    subgraph Despues["DESPUES: seam de parametro"]
      F2["price(item, today=None)"] --> EP{"today is None?"}
      EP -- "si (default)" --> C2["date.today()<br/>(llamador viejo: igual)"]
      EP -- "no (test)" --> C3["today fijo<br/>(test lo controla)"]
    end

Errores comunes

Reescribir la función para "quitarle" la dependencia en vez de abrir un seam. Qué pasa: para poder testear, el equipo decide extraer toda la lógica del cupón a otro lado, cambiar la firma, reorganizar el flujo —una cirugía grande— antes de tener un solo test. Por qué pasa: parece "hacerlo bien de una vez", y la dependencia soldada da la sensación de que hay que rediseñar. Cómo detectarlo: si para poner el primer test tienes que tocar veinte líneas y cambiar cómo se llama la función, estás reescribiendo, no abriendo una costura. Cómo corregirlo: busca el seam de menor riesgo —casi siempre, parametrizar la dependencia con un default que preserve el comportamiento—. Es un cambio de una o dos líneas, verificable (welded y seamed deben dar lo mismo), y suficiente para poner la red. La cirugía grande viene después, con la red ya puesta. Como el plomero: usa la junta, no la sierra.

Abrir un seam que sí cambia el comportamiento por defecto. Qué pasa: agregas el parámetro pero olvidas (o haces mal) el enabling point, de modo que el comportamiento por defecto cambia —por ejemplo, el default queda en una fecha fija en vez de date.today()—. Por qué pasa: es fácil equivocarse en el default, y como el test pasa (usa su propia fecha), no lo notas. Cómo detectarlo: la verificación de oro es comparar la versión vieja y la nueva sin usar el parámetro nuevo —como hizo la última sección del ejemplo: welded y seamed deben dar idéntico—. Si difieren, el seam está roto: cambió el comportamiento de producción. Cómo corregirlo: el default del seam tiene que reproducir exactamente lo que la función hacía antes. El enabling point (if today is None: today = date.today()) existe justo para eso. Un seam que altera el default no es una costura, es un cambio de comportamiento disfrazado —y de los peligrosos, porque se cuela sin que la red lo vea—.

Confundir el seam para testear con branch by abstraction. Qué pasa: al abrir la costura, el equipo aprovecha para meter ya la segunda implementación "moderna" detrás del punto de control, mezclando el objetivo de fijar con el de migrar. Por qué pasa: ambos insertan un punto de indirección, y "ya que abro la costura, aprovecho". Cómo detectarlo: si el seam que abriste para poder testear ya trae dentro una implementación nueva que reemplaza a la vieja, cruzaste al territorio del módulo 4 sin la red terminada. Cómo corregirlo: respeta el orden de la guía. Aquí, el seam tiene un solo propósito: darle al test control de la dependencia para poder fijar el comportamiento actual. La segunda implementación coexistiendo tras un flag —branch by abstraction— es el módulo 4, y se hace después de que la red esté completa. Mezclar los dos pasos te deja migrando sin haber terminado de fijar, que es justo lo que el módulo entero enseña a no hacer.

Ejercicios

Ejercicio 1 — Encuentra el seam. Esta función legacy de Mercado calcula el costo de envío y consulta un servicio externo de tarifas por dentro: def shipping_cost(order): rate = TariffAPI().get_rate(order.zone); return order.weight * rate. No puedes fijar su comportamiento porque cada corrida golpea el servicio real (lento, y con tarifas que cambian). Identifica la dependencia problemática, propón un seam concreto (di de qué tipo es) y escribe la nueva firma con su enabling point. Explica por qué los llamadores viejos no se enteran.

Ver solución

La dependencia problemática es TariffAPI().get_rate(order.zone): la función crea su propio cliente del servicio de tarifas y lo llama por dentro, así que cada corrida depende de un servicio externo lento y variable —imposible de fijar de forma determinista—.

El seam adecuado es un seam de objeto: en vez de crear el TariffAPI por dentro, la función lo recibe como parámetro (un colaborador que provee las tarifas), con un default que preserve el comportamiento viejo. Nueva firma con enabling point:

def shipping_cost(order, tariff_provider=None):
    if tariff_provider is None:            # enabling point: default = servicio real
        tariff_provider = TariffAPI()
    rate = tariff_provider.get_rate(order.zone)
    return order.weight * rate

Ahora el test puede pasar un tariff_provider de mentira que devuelve una tarifa fija (por ejemplo, un objeto con un get_rate que siempre retorna 5.0), y así fijar el comportamiento sin tocar el servicio real: shipping_cost(order, tariff_provider=FakeTariff(5.0)) es determinista y repetible.

Los llamadores viejos no se enteran porque no pasan el parámetro nuevo: para ellos, tariff_provider es None, el enabling point crea el TariffAPI() real igual que antes, y el comportamiento en producción es idéntico. La costura está ahí, cerrada por defecto; solo el test la abre. Igual que la junta del plomero: quien abre la llave sigue recibiendo agua como siempre.

Ejercicio 2 — El default que traiciona. Un compañero abre un seam en la función de precio así: def price(item, tax_rate=0.16): ..., poniendo la tasa de impuesto como parámetro con default 0.16. Su test pasa tax_rate=0.16 y sale verde, así que da el seam por bueno. Explica qué riesgo escondió y cómo lo detectarías. ¿En qué se diferencia de poner tax_rate=None con un enabling point que lea la global TAX_RATE?

Ver solución

El riesgo escondido es que el default 0.16 es un valor "quemado" (hardcoded) que puede no coincidir con la fuente real de la tasa en producción. Si el impuesto de Mercado vive en la global TAX_RATE (o en una configuración) y algún día vale 0.18, la función vieja usaría 0.18 —porque leía la global—, pero la función con el seam y default 0.16 seguiría usando 0.16 para todos los llamadores que no pasan el parámetro. El seam cambió el comportamiento por defecto: congeló la tasa en 0.16 sin que nadie lo decidiera. Y el test no lo detecta, porque el test pasa 0.16 explícitamente y ve verde —está probando un caso que casualmente coincide con el default, no el default real—.

Cómo detectarlo: la verificación de oro es comparar la función vieja y la nueva sin pasar el parámetro nuevo, y hacerlo en condiciones donde la global no valga 0.16. Si price_viejo(item) y price_nuevo(item) difieren cuando TAX_RATE es 0.18, el seam está roto.

La forma correcta es tax_rate=None con un enabling point que lea la fuente real: if tax_rate is None: tax_rate = TAX_RATE. Así el default reproduce lo que la función hacía antes (leer la global, sea cual sea su valor) en vez de suplantarlo con una constante. La diferencia es exactamente la del ejemplo del reloj: el enabling point con None preserva el comportamiento viejo; un default con valor quemado lo reemplaza. La regla: el default de un seam debe delegar en la fuente original, no adivinarla.

Ejercicio 3 — ¿Seam o branch by abstraction? Para cada situación, di si lo que se necesita es un seam para testear (este módulo) o branch by abstraction (módulo 4), y justifica: (a) quieres fijar el comportamiento del precio del catálogo, que consulta el reloj, sin cambiar lo que hace; (b) ya tienes la red completa y quieres reemplazar el cálculo de precio viejo por uno nuevo, con los dos coexistiendo tras un flag hasta confiar en el nuevo; (c) el precio golpea la BD para el descuento de categoría y no puedes escribir un test determinista.

Ver solución
  • (a) Fijar el precio que consulta el reloj, sin cambiar lo que hace → seam para testear (este módulo). El objetivo es caracterizar: darle al test control de la dependencia (el reloj) para poder fijar el comportamiento actual, sin alterarlo. Es exactamente el seam de parámetro del ejemplo. No hay una segunda implementación en juego; solo quieres poder tomar la foto.
  • (b) Reemplazar el cálculo viejo por uno nuevo, coexistiendo tras un flag → branch by abstraction (módulo 4). Aquí el objetivo ya no es testear sino migrar: tienes dos implementaciones (vieja y nueva) que conviven detrás de un punto de control (el flag), y vas moviendo el sistema de una a la otra hasta apagar la vieja. Eso es branch by abstraction, y se hace después de tener la red que este módulo construye —de hecho, la red de (a) es la que te dará confianza para hacer (b)—.
  • (c) El precio golpea la BD y no puedes testear determinista → seam para testear (este módulo). Igual que (a), pero con un seam de objeto: la dependencia es la BD, así que la inyectas como colaborador (un repositorio) para que el test le pase datos fijos. El objetivo sigue siendo caracterizar, no migrar.

La regla para distinguirlos: si lo que quieres es fijar el comportamiento actual dándole al test control de una dependencia, es un seam de este módulo. Si lo que quieres es reemplazar una implementación por otra con las dos coexistiendo, es branch by abstraction (módulo 4). Ambos insertan un punto de indirección, pero el propósito —testear vs migrar— es lo que los separa, y el orden importa: primero la red (seams), después la migración (branch by abstraction).

Resumen y siguiente paso

En esta lección resolviste el obstáculo que en la práctica frena la caracterización: el legacy soldado a su entorno, que no puedes fijar porque consulta el reloj, una global o la BD. La herramienta es el seam de Feathers —el punto donde alteras el comportamiento sin editar ahí mismo—, y su forma más segura es el seam de parámetro con un enabling point por default. Viste, con la junta del plomero, que un buen seam se abre sin cortar el tubo: los llamadores viejos no se enteran. Y lo mediste: la función de precio, que antes daba un resultado distinto según el día (imposible de fijar), quedó determinista al recibir today como parámetro —el test controla la fecha y sale verde hoy y siempre—, mientras que welded y seamed dan idéntico (104.4) para el llamador viejo, prueba de que el seam no cambió el comportamiento.

Antes de avanzar deberías poder: reconocer una dependencia soldada que impide fijar el comportamiento; elegir el seam adecuado (parámetro para valores, objeto para colaboradores) y escribir su enabling point; verificar que el seam no cambió el default comparando la versión vieja y la nueva sin el parámetro; y distinguir un seam para testear de branch by abstraction.

La lección 5 ataca el siguiente límite. Hasta ahora fijamos casos elegidos a mano —tres, cinco, un puñado—. Pero el catálogo real de Mercado recibe miles de combinaciones de precio, cantidad, categoría, cupón y lealtad, y ningún ser humano las va a enumerar una por una. Vas a aprender a muestrear el espacio de entradas y a grabar un golden master: una foto de cientos de salidas del legacy que, comparada contra una reimplementación candidata, acorrala exactamente dónde difieren —sin que sepas de antemano dónde buscar—.

Recursos

  • Michael Feathers, Working Effectively with Legacy Code (Prentice Hall, 2004), cap. 4 "The Seam Model" — la definición de seam, los tipos (object, function, link, preprocessing seams) y el concepto de enabling point. La fuente directa de esta lección. En inglés.
  • Martin Fowler, "Legacy Seam" — martinfowler.com/bliki/LegacySeam.html. Una síntesis breve del concepto de Feathers y de cómo abrir una costura para insertar tests en código no diseñado para ser testeable. En inglés.
  • Martin Fowler, "Inversion of Control" — martinfowler.com/bliki/InversionOfControl.html. El principio general detrás del seam de objeto: en vez de que el componente vaya a buscar sus dependencias, se las entregan desde afuera —lo que hace posible el test determinista—. En inglés.
  • Nicolas Carlo, "Seams to test your legacy code" — understandlegacycode.com. Ejemplos prácticos y modernos de cómo identificar y abrir seams en código real, en la línea directa de Feathers. En inglés.