Módulo 2: Entender y fijar el legacy

Presentación del módulo: entender y fijar el legacy

Por qué este módulo existe aquí

Hay un momento en toda modernización en que el discurso se acaba y hay que meter las manos en el código viejo. El módulo 1 te convenció, con números, de que la reescritura grande fracasa y de que el camino que gana es el incremental: mover el sistema por rebanadas mientras sigue vivo. Perfecto. Pero apenas te acercas a la primera rebanada aparece el problema que este módulo resuelve, y es incómodo: para cambiar una parte del legacy con seguridad necesitas poder cambiarla sin miedo, y hoy no puedes. El código de Mercado no tiene tests. Tocarlo es tocarlo a ciegas. Cualquier cambio —hasta el más pequeño y "obvio"— puede romper algo que no ves, en una parte del sistema que ni sabías que dependía de eso.

Este módulo instala la técnica que convierte ese código aterrador en código tocable. Se llama characterization test (también golden master o test de fijación), y hay que entender bien qué hace porque va contra el instinto de todo programador. Un characterization test no verifica que el código esté bien. Verifica que el código no cambie sin que lo notes. Toma la salida actual del legacy —con sus bugs, sus rarezas, sus redondeos raros y todo— y la congela como una foto. Desde ese momento, cualquier cambio que altere esa salida enciende el test en rojo. No dice "esto está correcto"; dice "esto sigue haciendo exactamente lo que hacía ayer". Y esa distinción, que parece un detalle, es justo lo que hace posible modernizar sin romper: primero congelas el comportamiento observable, después cambias el código por dentro, y la red te avisa al instante si se te fue una regresión.

Toda esta guía enseña a modernizar Mercado por rebanadas: ponerlo bajo red de seguridad (este módulo), desviar tráfico con un facade (módulo 3), migrar la implementación por dentro (módulo 4), extraer un servicio (módulo 5), migrar sus datos sin apagar el negocio (módulo 6) y medir el avance hasta apagar la vieja ruta (módulo 7). Pero fíjate en el orden: la red va primera. No se puede desviar tráfico con confianza, ni extraer un servicio, ni migrar datos, si no tienes una forma objetiva de saber que el comportamiento no cambió. Sin la red, cada paso de la migración es una apuesta. Con la red, cada paso es verificable. Por eso este módulo va donde va.

La rebanada que vamos a poner bajo la lupa es el módulo catalog de Mercado, y dentro de él, una función muy concreta y muy real: el cálculo de precio. Un precio de catálogo no es "precio por cantidad". Es precio por cantidad, menos descuento por categoría, menos descuento por volumen si compras muchas unidades, menos el cupón si aplica y no venció, más el impuesto, menos el descuento de lealtad si eres cliente gold... y cada una de esas reglas se agregó en un momento distinto, por una razón distinta, a lo largo de años. El resultado es una función que nadie entiende del todo y que tiene una rareza histórica: la lealtad gold se aplica después del impuesto, no antes como todos los demás descuentos. Parece un error. Y resulta que un socio de negocio depende de ese "error". Sobre esta función vamos a aprender todo el módulo.

Conexión con el módulo. Esta es la lección-mapa. Instala la tesis (antes de tocar código que da miedo, fija su comportamiento actual con un characterization test), el vocabulario (characterization test, seam, golden master, muestreo, la ley de Hyrum) y el mapa de cómo cada lección construye una pieza. La lección 2 explica por qué el código sin tests es "legacy" por definición y mide el costo del cambio a ciegas. La 3 enseña a escribir el characterization test —capturar lo que el legacy hace, no lo que debería—. La 4 busca el seam: el punto donde insertar una prueba o desviar una dependencia sin reescribir todo. La 5 escala la técnica con muestreo y golden master. La 6 enfrenta la rareza de la que alguien depende. La 7 une todo en la disciplina de leer y caracterizar antes de cambiar. Y la 8 te pone a fijar el catalog completo, ejecutado. La frontera, que es deliberada: la teoría de testing a fondo (unit vs integration, mocks, la pirámide) es el ecosistema de Testing; aquí los characterization tests son una herramienta de migración, no teoría. La mecánica del strangler (el router que desvía tráfico) es el módulo 3. Branch by abstraction (migrar por dentro tras una capa) es el módulo 4. Aquí solo construimos la red que hace posible todo eso.

Y la promesa de siempre: nada se afirma "de memoria", todo se ejecuta. Cada simulación corre con Python 3.14 y solo la biblioteca estándar, con datos y semillas fijas, así que la salida que ves en cada bloque "Qué esperar" es la salida literal de correr el código. Puedes copiarlo y reproducirlo idéntico.

Una analogía: la foto que tomas antes de renovar el baño

Vas a renovar el baño. Antes de romper el primer azulejo, cualquier plomero con experiencia hace lo mismo: saca el teléfono y toma fotos. Fotos de cómo está todo ahora. Y no solo de lo bonito —también, y sobre todo, de lo feo y lo raro—: por dónde entra la tubería de agua, dónde está la llave de paso, ese codo extraño que alguien soldó torcido hace veinte años, la grieta en la pared del fondo, el desagüe que va hacia un lado que no tiene sentido. ¿Por qué fotografía justamente las cosas raras? Porque cuando esté a medio romper la pared, con el polvo hasta las rodillas, la única forma de saber si acaba de romper algo importante es comparar con la foto de cómo estaba. La foto no dice "esto está bien construido". La grieta seguía siendo una grieta. El codo torcido seguía siendo un error. La foto dice otra cosa, mucho más útil: "así estaba antes de que yo tocara nada". Es la referencia contra la que va a medir cada cambio.

El characterization test es esa foto. Antes de tocar el código legacy, corres la función, capturas exactamente lo que devuelve —bugs incluidos, rarezas incluidas— y lo congelas. No estás diciendo que el código esté bien. Estás diciendo "así se comportaba antes de que yo tocara nada", y desde ese momento tienes contra qué comparar. Si mañana rompes el codo torcido sin querer, la foto —el test— te lo grita: "esto cambió". Y aquí está el punto que a todo el mundo le cuesta al principio: el plomero fotografía el codo torcido aunque esté mal, precisamente porque quizá está sosteniendo algo. En el software pasa igual. La rareza gold del catálogo de Mercado se ve como un error, pero la fotografías tal cual antes de tocar nada, porque hasta que no sepas quién depende de ella, romperla es más peligroso que dejarla.

Hay una segunda imagen, todavía más precisa, que vas a encontrar cuando hablemos de fijar sistemas enteros: el acta de estado de un departamento rentado. Cuando entras a rentar, tú y el dueño recorren el lugar anotando cada raspón, cada mancha, cada azulejo flojo —los feos también, sobre todo los feos—. No es para decir "este departamento es perfecto". Es para que, cuando te vayas, se pueda comparar y saber qué cambió por tu culpa y qué ya estaba. El acta protege registrando el estado real, warts and all. El characterization test es el acta de estado del legacy: fija el estado real, con verrugas y todo, para poder distinguir después lo que tú cambiaste de lo que ya venía así.

Ejemplo trabajado: la foto en verde, la reimplementación "limpia" en rojo

No vamos a afirmar que un characterization test atrapa regresiones. Lo vamos a ver pasar. Toma la función de precio del catálogo de Mercado, tal como está hoy. Léela sin juzgarla: cada línea es una regla que alguien agregó por una razón. Fíjate especialmente en dos cosas: que redondea hacia abajo al centavo en cada paso (_floor_cents), un hábito viejo de la casa, y que la lealtad gold se aplica después del impuesto —la rareza—.

import math

TAX_RATE = 0.16
CATEGORY_DISCOUNT = {"electronics": 0.05, "books": 0.10, "toys": 0.08, "grocery": 0.00}
BULK_MIN_QTY = 10
BULK_DISCOUNT = 0.05
GOLD_LOYALTY_DISCOUNT = 0.03


def _floor_cents(amount):
    # Rareza historica de la casa: cada paso redondea HACIA ABAJO al centavo.
    return math.floor(amount * 100) / 100


def legacy_catalog_price(item):
    # El precio tal como lo cobra el monolito HOY. No lo juzgamos: lo describimos.
    subtotal = _floor_cents(item["unit_price"] * item["quantity"])
    price = _floor_cents(subtotal * (1 - CATEGORY_DISCOUNT.get(item["category"], 0.0)))
    if item["quantity"] >= BULK_MIN_QTY:
        price = _floor_cents(price * (1 - BULK_DISCOUNT))
    price = _floor_cents(price * (1 - item.get("coupon_percent", 0.0)))
    price = _floor_cents(price * (1 + TAX_RATE))
    if item.get("loyalty") == "gold":
        price = _floor_cents(price * (1 - GOLD_LOYALTY_DISCOUNT))  # gold DESPUES del impuesto
    return price


def clean_catalog_price(item):
    # Reimplementacion "limpia": aplica la lealtad gold ANTES del impuesto,
    # como todos los demas descuentos. Se ve mas coherente... y cambia el
    # resultado de los clientes gold, una rareza de la que alguien dependia.
    subtotal = _floor_cents(item["unit_price"] * item["quantity"])
    price = _floor_cents(subtotal * (1 - CATEGORY_DISCOUNT.get(item["category"], 0.0)))
    if item["quantity"] >= BULK_MIN_QTY:
        price = _floor_cents(price * (1 - BULK_DISCOUNT))
    price = _floor_cents(price * (1 - item.get("coupon_percent", 0.0)))
    if item.get("loyalty") == "gold":
        price = _floor_cents(price * (1 - GOLD_LOYALTY_DISCOUNT))  # gold ANTES del impuesto
    price = _floor_cents(price * (1 + TAX_RATE))
    return price


ITEMS = [
    {"sku": "A-1", "unit_price": 100.00, "quantity": 1,  "category": "electronics"},
    {"sku": "B-2", "unit_price": 19.99,  "quantity": 3,  "category": "books"},
    {"sku": "C-3", "unit_price": 7.50,   "quantity": 12, "category": "toys"},
    {"sku": "D-4", "unit_price": 100.00, "quantity": 1,  "category": "electronics",
     "loyalty": "gold"},
    {"sku": "E-5", "unit_price": 49.00,  "quantity": 2,  "category": "books",
     "coupon_percent": 0.15},
]

# 1) Fijamos el comportamiento ACTUAL del legacy como "verdad de referencia".
GOLDEN = {it["sku"]: legacy_catalog_price(it) for it in ITEMS}


def run_characterization(price_fn, label):
    print(f"characterization_test contra {label}:")
    passed = failed = 0
    for it in ITEMS:
        expected = GOLDEN[it["sku"]]
        actual = price_fn(it)
        if actual == expected:
            print(f"  PASS  {it['sku']}  esperado={expected:<8} actual={actual}")
            passed += 1
        else:
            print(f"  FAIL  {it['sku']}  esperado={expected:<8} actual={actual}  "
                  f"(diff={round(actual - expected, 2)})")
            failed += 1
    verdict = "VERDE (0 fallos)" if failed == 0 else f"ROJO ({failed} fallo(s))"
    print(f"  -> {passed} passed, {failed} failed  ==> {verdict}\n")


print("Precios de referencia fijados desde el legacy (el 'como funciona hoy'):")
for sku, p in GOLDEN.items():
    print(f"  {sku}: {p}")
print()

run_characterization(legacy_catalog_price, "el LEGACY (el que caracterizamos)")
run_characterization(clean_catalog_price, "la reimplementacion 'LIMPIA'")

Qué esperar. Al correr el archivo, la salida es exactamente esta:

Precios de referencia fijados desde el legacy (el 'como funciona hoy'):
  A-1: 110.19
  B-2: 62.6
  C-3: 91.24
  D-4: 106.88
  E-5: 86.96

characterization_test contra el LEGACY (el que caracterizamos):
  PASS  A-1  esperado=110.19   actual=110.19
  PASS  B-2  esperado=62.6     actual=62.6
  PASS  C-3  esperado=91.24    actual=91.24
  PASS  D-4  esperado=106.88   actual=106.88
  PASS  E-5  esperado=86.96    actual=86.96
  -> 5 passed, 0 failed  ==> VERDE (0 fallos)

characterization_test contra la reimplementacion 'LIMPIA':
  PASS  A-1  esperado=110.19   actual=110.19
  PASS  B-2  esperado=62.6     actual=62.6
  PASS  C-3  esperado=91.24    actual=91.24
  FAIL  D-4  esperado=106.88   actual=106.89  (diff=0.01)
  PASS  E-5  esperado=86.96    actual=86.96
  -> 4 passed, 1 failed  ==> ROJO (1 fallo(s))

Léelo despacio, porque en esas dos corridas está todo el módulo en miniatura.

La primera corrida es el test contra el propio legacy, y sale verde: 5 de 5. Tiene que salir verde —capturamos los valores esperados del mismo legacy, así que por construcción coinciden—. Esto no es trivial ni tonto: es el paso que fija la foto. Esos cinco números (110.19, 62.6, 91.24, 106.88, 86.96) son ahora el "cómo funciona hoy" del catálogo. No afirmamos que sean los precios "correctos" —el 110.19 sale de redondear hacia abajo un montón de veces, y el 106.88 encierra la rareza gold—. Afirmamos algo distinto y más útil: que así se comporta el legacy en este instante, y que a partir de ahora tenemos contra qué comparar.

La segunda corrida es donde la red hace su trabajo. Alguien —con la mejor intención— reimplementó el precio "limpio": movió la lealtad gold a antes del impuesto, para que sea coherente con los demás descuentos. Se ve más ordenado. Y mira lo que pasa: cuatro de los cinco casos pasan —A-1, B-2, C-3, E-5 dan idéntico, porque ninguno es cliente gold, así que la rama que cambió ni se ejecuta—. Pero D-4 falla: el cliente gold. El legacy cobraba 106.88; la versión "limpia" cobra 106.89. Un centavo. La red lo atrapa y pinta la corrida de rojo.

Ese centavo es el corazón del módulo. La reimplementación "limpia" no es un desastre evidente —pasa el 80% de los casos, y el que falla, falla por un centavo—. Si no tuvieras la red, se iría a producción sin que nadie parpadeara. Y tres semanas después, el socio de lealtad de Mercado —que factura contra ese número exacto— reclama que las cuentas no cuadran. El characterization test convirtió una regresión invisible de un centavo, que habría estallado semanas después como una disputa de negocio, en un rojo inmediato, aquí, ahora, antes de tocar producción. Esa es la promesa entera de la técnica: no te dice qué está bien; te dice qué cambió, al instante.

Las piezas que instala este módulo, y dónde vive cada una

Ese ejemplo tocó, sin desarrollarlas, las ideas del módulo. Vale la pena verlas explícitas, porque son la columna vertebral de las siete lecciones que siguen.

1. Código sin tests = legacy, y cambiarlo a ciegas es carísimo (lección 2). La definición de Feathers no habla de antigüedad: un código escrito ayer sin tests ya es legacy, porque no puedes cambiarlo con confianza. La lección 2 mide el costo real del cambio a ciegas —un "arreglo" inofensivo que toca el 88% de las órdenes sin que nadie lo note— y por qué la red lo cambia todo.

2. El characterization test fija lo actual, no lo ideal (lección 3). La técnica central, y su trampa: capturas lo que el legacy hace, no lo que debería hacer. Adivinar el valor "correcto" es el error clásico. La lección 3 enseña el flujo de captura y muestra que el test fija la rareza a propósito.

3. El seam: dónde insertar la prueba (lección 4). A veces no puedes fijar el comportamiento porque una dependencia soldada —un reloj, una global— lo hace impredecible. El seam de Feathers es el punto donde desvías esa dependencia sin reescribir. La lección 4 lo introduce sobre el reloj del cupón.

4. Golden master y muestreo, para cuando las entradas son muchas (lección 5). Cinco casos a mano no bastan para un catálogo real. Muestreas el espacio de entradas y grabas un golden master de cientos de salidas. La lección 5 corre un parallel-run de 500 casos que acorrala la rareza sin saber de antemano dónde estaba.

5. La rareza de la que alguien depende (lección 6). La ley de Hyrum: con suficientes consumidores, cada comportamiento observable se vuelve un contrato. La lección 6 muestra al socio de lealtad que depende del centavo gold, y qué hacer en vez de "arreglar" el bug.

6. La disciplina completa (lección 7). Leer → encontrar el seam → caracterizar → cambiar → dejar que la red decida. La lección 7 corre un refactor seguro (verde) y un cambio de comportamiento (rojo) bajo la misma red.

Guarda este mapa; es la ruta del módulo:

Idea                                       Lección   Concepto clave
─────────────────────────────────────────  ────────  ──────────────────────────────
Código sin tests = legacy; cambio a ciegas  L2        la paradoja del cambio,
                                                      el costo del cambio ciego
El characterization test fija lo ACTUAL     L3        capturar, no adivinar;
                                                      fijar la rareza a proposito
Encontrar el seam                           L4        parametrizar la dependencia,
                                                      enabling point sin romper
Golden master y muestreo                    L5        parallel_run, acorralar la
                                                      discrepancia
─────────────────────────────────────────  ────────  ──────────────────────────────
La rareza de la que alguien depende         L6        la ley de Hyrum, el bug-feature
Leer y caracterizar antes de cambiar        L7        el loop de cambio seguro
Fijar el precio del catalogo                L8        el mini-proyecto, ejecutado

El mapa: dónde está este módulo en la guía y en el ecosistema

Este módulo es la base sobre la que se apoya toda la mecánica de la guía. Así se conecta:

flowchart TD
    M1["M1 · Por qué no reescribir<br/>(la conviccion: incremental gana)"]
    M2["M2 · Entender y fijar el legacy<br/>(la red de seguridad)"]
    M3["M3 · El patrón strangler fig<br/>(la mecánica del router)"]
    M4["M4 · Branch by abstraction"]
    M5["M5 · Extraer un servicio"]
    M6["M6 · Migrar datos sin downtime"]
    M7["M7 · Medir el progreso de la migración"]
    M8["M8 · Proyecto: modernizar una rebanada de Mercado"]
    M1 --> M2 --> M3 --> M4 --> M5 --> M6 --> M7 --> M8

Léelo así: en M1 te convenciste de que el incremental gana; aquí, en M2, pones el legacy bajo una red de seguridad (characterization tests) para poder tocarlo sin miedo; en M3 aprendes la mecánica del strangler (el router que desvía tráfico) —y ese desvío solo es seguro porque la red de M2 te avisa si el nuevo servicio no reproduce el comportamiento viejo—; en M4 migras la implementación por dentro; en M5 extraes un servicio; en M6 migras sus datos; en M7 mides el avance. La red que construyes aquí es la que hace verificables todos los pasos que siguen.

Y la frontera con las guías hermanas, que hay que respetar. La teoría de testing —qué es un unit test, cómo funcionan los mocks, la pirámide de tests, integration vs contract testing— la enseña a fondo el ecosistema de Testing; cuando aquí escribamos un characterization test, no vamos a re-enseñar qué es una aserción ni cómo se estructura una suite: vamos a usar el test como herramienta de migración, para fijar comportamiento antes de cambiarlo. La mecánica del strangler (el router, los porcentajes de tráfico) es el módulo 3; branch by abstraction (migrar por dentro tras una capa de abstracción) es el módulo 4. Y la decisión de migrar y su registro —el ADR, el costo— vive en architecture-decisions-and-tradeoffs. Aquí, solo la red.

Errores comunes

Creer que un characterization test dice que el código está "bien". Qué pasa: alguien ve la suite en verde y concluye "el precio del catálogo está correcto, el test lo prueba". Por qué pasa: estamos entrenados para que "test en verde" signifique "código correcto" —así funcionan los tests que escribimos después de decidir qué debe hacer el código—. Pero un characterization test es lo contrario: se escribe capturando lo que el código ya hace, sin juzgarlo. Cómo detectarlo: si alguien usa la suite de caracterización como argumento de que un comportamiento es el deseado ("está bien porque el test pasa"), está confundiendo las dos cosas. Cómo corregirlo: recuerda la foto del baño —fotografiaba el codo torcido aunque estuviera mal—. El verde de un characterization test significa "no cambió respecto a ayer", no "es correcto". Un bug caracterizado sale en verde porque el bug sigue ahí, exactamente como estaba. Su valor no es certificar correctitud; es detectar cambio. Si además quieres saber si el comportamiento es el deseado, esa es una pregunta de negocio aparte —la lección 6 la trabaja con la rareza gold—.

Empezar a cambiar el legacy antes de fijarlo. Qué pasa: el equipo abre la función de precio, ve el redondeo raro y la rareza gold, y empieza a "limpiar" mientras la lee, sin capturar primero el comportamiento actual. Por qué pasa: el impulso de arreglar lo feo es fortísimo, y "ya que estoy aquí" se siente eficiente. Cómo detectarlo: si el primer commit sobre una pieza legacy cambia comportamiento en vez de fijarlo, el orden está invertido. Cómo corregirlo: la regla del módulo —y de este oficio— es caracterizar antes de cambiar, siempre. Primero la foto, después la obra. Sin la foto, cuando algo se rompa no vas a poder distinguir lo que rompiste tú de lo que ya estaba mal, y vas a "arreglar" cosas que en realidad eran contratos. El ejemplo de esta lección lo mostró: la versión "limpia" se veía mejor y rompía un contrato de un centavo. Si la hubieras desplegado sin la foto, lo habrías descubierto en la disputa con el socio, no en el test.

Ejercicios

Ejercicio 1 — ¿Verde significa correcto? El equipo de Mercado corre el characterization test del catálogo y sale verde: 5 de 5. Un gerente concluye: "perfecto, entonces el cálculo de precios está bien, podemos seguir". Explica por qué esa conclusión es un malentendido de lo que el test verifica, y reformula lo que el verde garantiza. Luego di qué evidencia adicional haría falta para afirmar que el precio está "correcto".

Ver solución

El malentendido es tratar "el characterization test pasa" como "el comportamiento es el deseado". No lo es. Ese test se construyó capturando la salida actual del legacy —incluyendo el redondeo hacia abajo en cada paso y la rareza de aplicar gold después del impuesto—. Por construcción, esos valores raros están dentro de la referencia. El verde solo puede significar una cosa: la salida de hoy es idéntica a la salida que capturamos —el código no cambió su comportamiento observable—. Un bug perfectamente caracterizado también sale verde, porque el bug sigue ahí igual que ayer.

Lo que el verde garantiza, dicho con precisión: "nadie alteró, sin querer o queriendo, lo que esta función devuelve para los casos que fijamos". Es una garantía de no-cambio, no de correctitud.

Para afirmar que el precio está "correcto" haría falta evidencia de otra naturaleza: la especificación de negocio de cómo debe calcularse el precio (¿gold antes o después del impuesto?, ¿redondeo al centavo hacia abajo o al más cercano?), y una comparación de la salida contra esa especificación. Eso es una decisión de producto/negocio, no algo que un characterization test pueda responder. La lección 6 muestra el caso más delicado: a veces el comportamiento "raro" es el correcto según el negocio, porque alguien ya depende de él.

Ejercicio 2 — ¿Por qué D-4 falla y los otros no? En la segunda corrida del ejemplo, la reimplementación "limpia" pasa en A-1, B-2, C-3 y E-5, pero falla en D-4. Explica, mirando el código de las dos funciones, por qué exactamente ese caso —y solo ese— cambia de resultado. ¿Qué tienen en común los cuatro que pasan, y qué tiene de distinto el que falla?

Ver solución

La única diferencia entre legacy_catalog_price y clean_catalog_price es el orden en que se aplica la lealtad gold respecto al impuesto: el legacy la aplica después del impuesto; la versión "limpia", antes. Esa diferencia solo puede producir un resultado distinto si el ítem es cliente gold —o sea, si la rama if item.get("loyalty") == "gold" se ejecuta—.

  • A-1, B-2, C-3, E-5 no tienen loyalty == "gold". Para ellos, la rama gold no se ejecuta en ninguna de las dos funciones, así que las dos hacen exactamente los mismos pasos en el mismo orden: subtotal, categoría, (volumen si aplica), cupón, impuesto. Resultado idéntico → PASS.
  • D-4 sí es gold. En el legacy: aplica impuesto (95.00 → ~110.19/110.20) y luego el 3% de gold. En la versión limpia: aplica el 3% de gold antes del impuesto. Como cada paso redondea hacia abajo al centavo, el orden cambia dónde caen los centavos, y el resultado final difiere por un centavo: 106.88 (legacy) vs 106.89 (limpia) → FAIL.

La lección para llevar: un cambio que parece afectar "solo el orden de dos operaciones" puede tener un radio de impacto muy concreto —aquí, únicamente los clientes gold—. Sin una red que pruebe casos gold, ese cambio pasaría desapercibido para todos los demás y solo lo sentiría el segmento afectado. La lección 5 muestra por qué necesitas muestrear muchas entradas para garantizar que casos como D-4 estén cubiertos.

Ejercicio 3 — La foto antes de la obra. Un compañero va a "mejorar" el cálculo de envío del módulo shipping de Mercado (otra función legacy sin tests). Su plan: "abro la función, la reescribo más limpia, corro el sistema a ver si truena, y si no truena, listo". Identifica qué le falta a ese plan usando la analogía de la foto, y describe el primer paso que debería dar antes de tocar una línea.

Ver solución

Lo que le falta es la foto: no tiene forma de saber si su versión "más limpia" se comporta igual que la vieja, porque no capturó el comportamiento viejo antes de tocarlo. Su criterio —"si no truena, listo"— es peligrosamente débil por dos razones. Primero, "no truena" solo detecta fallas ruidosas (una excepción, un crash); las regresiones más caras del legacy son silenciosas —un número que cambia por un centavo, un caso raro que ahora se calcula distinto— y esas no truenan, se despliegan en verde y estallan semanas después (justo lo que le pasó a la versión "limpia" del catálogo con el cliente gold). Segundo, "correr el sistema a ver" prueba, con suerte, el camino feliz que él pensó, no los cientos de combinaciones de entrada que el shipping real recibe.

El primer paso que sí debería dar, antes de tocar una sola línea: caracterizar. Correr el shipping legacy con una batería de entradas representativas (distintos pesos, destinos, cantidades, casos borde), capturar exactamente lo que devuelve hoy —feo o no— y fijarlo como golden master. Solo entonces reescribir, corriendo la red después de cada cambio. Si la red se queda verde, su versión limpia reproduce el comportamiento y puede desplegarse con confianza; si se pone roja, encontró una diferencia que tiene que decidir a conciencia (¿bug que arreglo, o contrato que preservo?). Primero la foto, después la obra —el orden no es negociable—.

Resumen y siguiente paso

En esta lección instalaste la tesis que sostiene todo el módulo: antes de tocar código legacy que da miedo, fija su comportamiento actual con una red de seguridad. Viste, con la foto del baño, que la referencia no dice "esto está bien" sino "así estaba antes de que yo tocara nada" —y que fotografías hasta el codo torcido, porque quizá sostiene algo—. Y lo mediste: capturaste los cinco precios del catálogo de Mercado como golden master, viste el characterization test en verde contra el propio legacy, y lo viste en rojo contra una reimplementación "limpia" que rompió, por un centavo, el caso del cliente gold —una regresión que sin la red se habría ido a producción y habría estallado semanas después—.

Antes de avanzar deberías poder: explicar por qué un characterization test verifica no-cambio y no correctitud; distinguir "fijar el comportamiento" de "arreglar el código", y por qué el orden importa; y describir cómo una regresión de un centavo, invisible en el camino feliz, puede ser un contrato de negocio roto.

La lección 2 abre la pregunta de fondo: ¿por qué es tan caro cambiar código sin tests? Vas a ver la definición de Feathers —código sin tests es legacy sin importar su edad— y vas a medir, con el catálogo de Mercado, el costo exacto del cambio a ciegas: un "arreglo" de redondeo aparentemente inofensivo que cambia el precio del 88% de las órdenes y se despliega en verde sin que nadie lo note, contra el mismo cambio con una red que lo enciende en rojo al instante.

Recursos

  • Michael Feathers, Working Effectively with Legacy Code (Prentice Hall, 2004) — la biblia del tema. Su definición ("legacy code es código sin tests") y su técnica del characterization test son el corazón de este módulo. Los capítulos 4 (seams) y 13 (caracterización) son la base directa de las lecciones 3 y 4. En inglés.
  • "Characterization test" — término de Michael Feathers (Working Effectively with Legacy Code); resumen en Wikipedia: en.wikipedia.org/wiki/Characterization_test. Una explicación corta y clara sobre qué es y qué no es un characterization test: la distinción entre fijar comportamiento y verificar correctitud, que esta lección instala. En inglés.
  • Martin Fowler, "Patterns of Legacy Displacement" — martinfowler.com/articles/patterns-legacy-displacement. Correr la implementación vieja y la nueva en paralelo y comparar sus salidas; el parallel run como patrón nombrado es de Sam Newman (Monolith to Microservices). Exactamente lo que hizo la segunda corrida del ejemplo. La lección 5 lo desarrolla con golden master. En inglés.
  • Michael Feathers, ensayos y charlas — michaelfeathers.silvrback.com. Escritos cortos del autor sobre legacy, seams y caracterización; buen acompañamiento para todo el módulo. En inglés.
  • Sam Newman, Monolith to Microservices (O'Reilly, 2019), cap. 3 — cómo poner una costura alrededor de una pieza del monolito antes de extraerla. El puente entre este módulo (fijar) y el 5 (extraer). En inglés.