EN · Read in English Explorar gratis

Receta · Intermedio · 20 min

Refactorizar código legado con Claude Code sin romper nada

Método en 5 pasos para refactorizar código heredado con Claude Code: modo plan, tests de caracterización, commits pequeños y qué deshace /rewind.

Portada: Refactorizar código legado con Claude Code sin romper nada

Probado con Claude Code 2.1.316 · revisado

Ver los archivos en GitHub Gratis · licencia MIT

El problema

Este código tiene 12 años y nadie quiere tocarlo. No tiene tests, nadie recuerda por qué hace lo que hace y otro código depende de sus rarezas. Refactorizar código heredado sin tests es la forma más rápida de romper producción, con o sin IA, y con un agente que edita rápido el riesgo es mayor si no hay red. La receta aplica con Claude Code el método clásico de Michael Feathers: entender antes de tocar, fijar el comportamiento actual con tests de caracterización, cambiar en pasos pequeños y verificados, y tener siempre un camino de vuelta con git y /rewind, sabiendo lo que /rewind no deshace.

La solución

El ejemplo es real: una función de facturación de 2014. Todos los archivos (el código heredado, unos tests de caracterización de ejemplo, los prompts y el bloque de CLAUDE.md) están en el repositorio gratuito ctrlia-refactorizar-codigo-legado.

# facturas.py: escrito en 2014, nadie lo toca
def total(lineas, cliente):
    t = 0
    for l in lineas:
        if l[2] == None:
            l[2] = 1
        t = t + l[1] * l[2]
    if cliente == "VIP":
        t = t - t * 10 / 100
    if t > 1000:
        t = t - 50
    return round(t, 2)

1. Explora sin tocar: modo plan

Antes de cambiar nada, pide a Claude que entienda el código y te lo cuente. Pulsa Shift+Tab hasta ver «plan mode on» en la barra de estado: en modo plan Claude lee y propone, pero no edita. Pídele algo concreto:

Explica qué hace facturas.total, quién la llama (busca en todo el repo), qué datos recibe y qué efectos secundarios tiene. No propongas cambios todavía.

Si el código es grande, pide que use un subagente para la búsqueda de llamadores: el resultado llega resumido y tu conversación no se llena de salida de grep (lo explicamos en la receta de subagentes). Lo que buscas en este paso es el mapa: entradas, salidas, efectos secundarios (escribe en disco, modifica argumentos, llama a la red) y quién depende de qué. Apunta en un archivo REFACTOR.md lo que vas descubriendo; te servirá si la sesión se compacta o la retomas otro día. El vídeo oficial de Anthropic resume el patrón como «Explore → Plan → Code → Commit», y la guía de flujos de trabajo recomienda refactorizar en incrementos pequeños y comprobables.

2. Tests de caracterización: fija lo que hace hoy

Un test de caracterización no comprueba lo que el código debería hacer, sino lo que hace hoy, rarezas incluidas: otro código puede depender de ellas. Pide los tests y prohíbe tocar el código:

git add -A && git commit -m "antes del refactor"

claude --disallowedTools "Edit(facturas.py)"
# > Escribe tests de CARACTERIZACIÓN en tests/test_facturas.py (unittest) para facturas.total:
#   deben fijar el comportamiento ACTUAL, aunque parezca un error, no el que debería tener.
#   Cubre: cantidad None, cliente VIP, el descuento de 50 al pasar de 1000, el orden
#   VIP-luego-50, lista vacía y si la función modifica la lista de entrada.
#   No toques facturas.py. Ejecuta los tests (deben pasar todos) y al final lista
#   en 3 viñetas las rarezas que encontraste.

python3 -m unittest discover -s tests -t .
git add tests && git commit -m "test: caracterización de facturas.total"

Enumera, como en el prompt, los casos que te preocupan (valores nulos, descuentos, bordes, listas vacías, efectos secundarios) y pide que al final liste las rarezas encontradas. En nuestra prueba, sobre esta función de 12 líneas, Claude escribió 10 tests que pasaban y encontró tres cosas que nadie había documentado:

- Muta la entrada: sustituye cantidad None por 1 directamente en las listas que recibe, así que el llamador ve el cambio.
- El tipo de retorno cambia: con lista vacía devuelve int (0) sin VIP y float (0.0) con VIP. Además, el descuento de 50 se decide sobre el total ya rebajado por VIP, así que VIP puede anular ese descuento.
- Validación mínima: la comparación con "VIP" distingue mayúsculas, cantidad 0 se mantiene en 0 y un precio None lanza TypeError. Estos tres puntos los leí en el código y no los fijé en tests.

Fíjate en la última viñeta: también avisó de lo que leyó en el código pero no fijó en tests, como que «vip» en minúsculas no recibe descuento. Revisa la lista y decide qué rarezas son errores y cuáles son contrato; haz commit de los tests. En el repositorio tienes un tests/test_facturas.py de ejemplo con 12 tests que fijan todas esas rarezas, incluidas las tres que Claude no fijó, para que compares con los tuyos.

3. Cambia en pasos pequeños, uno por commit

Ahora sí, refactoriza, pero un cambio cada vez. Primero los que no cambian el comportamiento: renombrar l y t, sacar el descuento VIP a una función, sustituir == None por is None. Después de cada cambio, Claude ejecuta los tests de caracterización y, si pasan, haces commit. Si fallan, se descarta ese cambio y no se sigue. Un buen prompt para cada paso:

Haz SOLO este cambio: <cambio concreto, p. ej. extrae el descuento VIP a una función aplicar_vip>.
No cambies el comportamiento ni edites nada en tests/.
Ejecuta python3 -m unittest discover -s tests -t . y enséñame la salida.
Si algún test falla, deshaz el cambio y dime por qué ha fallado.

Pide que no edite los tests en esta fase: es la misma regla de la receta de TDD con Claude Code, con Edit(tests/**) denegado. Arreglar las rarezas que decidiste que son errores es otra fase: primero cambias el test de caracterización para que describa el comportamiento correcto (se pone rojo), haces commit, y luego pides el arreglo, como en TDD. Así cada cambio de comportamiento queda explícito en el historial, separado de los cambios de forma. Si el módulo es grande, empieza por la función que más se usa y menos se entiende.

4. Ten un camino de vuelta: qué deshace /rewind y qué no

Claude Code guarda un punto de control antes de cada prompt que envías. /rewind (o Esc dos veces con la entrada vacía) abre un menú con tus prompts y te deja restaurar el código, la conversación o ambos a ese punto; se guardan los 100 últimos y siguen ahí si reanudas la sesión. Pero la documentación de checkpointing lista límites que importan mucho en un refactor:

La propia documentación dice que no sustituye al control de versiones. Por eso el orden es: commit antes de cada cambio, /rewind para el «uy» rápido dentro de la sesión, y git restore o git revert para todo lo demás.

5. Deja el terreno preparado para la próxima vez

Al terminar, guarda lo aprendido donde Claude lo vuelva a leer. En el CLAUDE.md del proyecto, añade una sección corta sobre el módulo: el comando de sus tests, las rarezas que son contrato («total() devuelve int con lista vacía: lo usa el informe mensual») y las que quedan pendientes:

## facturas.py (código heredado)
- Tests: `python3 -m unittest tests.test_facturas` (caracterización: fijan el comportamiento real).
- Contrato que NO se cambia sin avisar: total() devuelve int con lista vacía y sin VIP.
- Pendiente: total() modifica la lista de entrada (decidir si es error).
- Un cambio por commit; cambios de comportamiento siempre con el test cambiado antes.

La guía oficial de buenas prácticas da un ejemplo de este tipo de nota: una carpeta legacy que sigue en producción y cuyos patrones no deben copiarse en código nuevo (más en un CLAUDE.md que funciona). Mantén los tests de caracterización en el repositorio: son la documentación más fiable del comportamiento real y avisarán si alguien lo cambia sin querer. Si el refactor fue largo, pide a Claude que resuma en REFACTOR.md qué cambió, en qué commits y qué queda por hacer, y bórralo cuando lo integres. Para refactors grandes en paralelo (varios módulos a la vez), mira la receta de subagentes; y si quieres lanzar los tests de caracterización cada noche, la de claude -p en cron.

Compruébalo

Con el ejemplo del repositorio:

cd ejemplo
python3 -m unittest discover -s tests -t .

Salida esperada:

............
----------------------------------------------------------------------
Ran 12 tests in 0.000s

OK

Para ver que los tests te protegen, haz dos cambios. Primero uno de forma: extrae el descuento a aplicar_vip, renombra l a linea y cambia == None por is None; los 12 tests siguen en OK. Después uno de comportamiento: haz que la función deje de modificar la lista de entrada; falla test_rareza_modifica_la_lista_de_entrada. Eso es lo que quieres: un cambio de comportamiento nunca pasa sin que lo veas.

Lo que comprobamos con Claude Code 2.1.316 en Linux el 1-nov-2026: claude -p --model haiku --disallowedTools "Edit(facturas.py)" escribió 10 tests de caracterización con unittest que pasaron, y listó las rarezas de arriba (0,01 USD estimados). No ejecutamos /rewind, porque es interactivo: sus límites salen de la documentación oficial de checkpointing.

Por qué funciona

Los tests de caracterización convierten el comportamiento actual, que solo existía en el código, en algo ejecutable: cualquier cambio que lo altere se pone rojo al momento. Bloquear facturas.py con --disallowedTools mientras se escriben obliga a que los tests describan el código tal como está, no como Claude cree que debería estar. Los cambios pequeños con un commit cada uno hacen que, si algo falla, sepas exactamente qué lo rompió y puedas deshacerlo con git, que cubre lo que /rewind no cubre (Bash, subagentes y cambios externos). Más detalles en checkpointing, flujos de trabajo comunes, buenas prácticas, modos de permisos y permisos; y en nuestra receta de permisos de Claude Code.

Errores típicos

Preguntas frecuentes

¿Por qué fijar errores en los tests en vez de arreglarlos ya?

Porque otro código puede depender de ellos. Primero los fijas, luego decides uno por uno si son error o contrato, y los arreglas como un cambio explícito con su test.

¿/rewind me salva si algo sale mal?

Solo en parte: deshace las ediciones de Claude con sus herramientas de archivos, pero no los archivos que solo cambió Bash, normalmente tampoco las ediciones de subagentes ni los cambios externos. Haz commit antes de cada paso.

El módulo es enorme. ¿Por dónde empiezo?

Por la función que más se usa y menos se entiende. Explora en modo plan, usa un subagente para buscar llamadores y caracteriza solo esa función antes de pasar a la siguiente.

¿Y si el código no tiene tests ni forma fácil de ejecutarlo?

Pide primero el arnés mínimo: un script que llame a la función con datos de ejemplo y guarde la salida (golden master). Compara esa salida antes y después de cada cambio.

¿Funciona con PHP, Java o COBOL?

El método sí; cambia el framework de tests. Pon en CLAUDE.md el comando exacto para ejecutarlos y cómo lanzar un solo archivo.

Toda la biblioteca