6. Refactorización segura después de una barra verde

6.1 Objetivo del tema

En este tema trabajaremos la tercera etapa del ciclo de TDD: refactor. Ya tenemos pruebas en verde, por lo tanto podemos mejorar el código con menor riesgo.

Refactorizar no significa agregar comportamiento nuevo. Significa cambiar la estructura interna, los nombres o la organización del código sin modificar lo que el programa hace desde el punto de vista de las pruebas.

Objetivo práctico: mejorar el validador de contraseñas y sus pruebas ejecutando pytest después de cada cambio pequeño.

6.2 Punto de partida

Partimos de una suite en verde. El código de producción valida solamente la longitud mínima de la contraseña.

Archivo existente: src/seguridad/password.py

LONGITUD_MINIMA = 8


def es_password_valido(password):
    return len(password) >= LONGITUD_MINIMA

Antes de refactorizar ejecutamos:

python -m pytest

Si la suite no está en verde, no es momento de refactorizar.

6.3 Pruebas actuales

El archivo de pruebas puede estar así:

Archivo existente: tests/test_password.py

from seguridad.password import es_password_valido


def test_password_es_valido_si_tiene_ocho_caracteres():
    assert es_password_valido("abcdefgh") is True


def test_password_es_valido_si_tiene_mas_de_ocho_caracteres():
    assert es_password_valido("abcdefghi") is True


def test_password_es_invalido_si_tiene_menos_de_ocho_caracteres():
    assert es_password_valido("abc") is False

Estas pruebas cubren el comportamiento actual: menos de 8 caracteres es inválido, 8 o más caracteres es válido.

6.4 Regla principal del refactor

Durante el refactor no cambiamos el comportamiento observable. Por eso, después de cada modificación, ejecutamos la suite completa.

python -m pytest

Si algo falla, el último cambio introdujo un problema. Como los pasos son pequeños, encontrar la causa es mucho más fácil.

6.5 Primer refactor: mejorar nombres internos

El nombre password dentro de la función funciona, pero en un curso en castellano puede ser más claro usar contrasena. Este cambio no altera el comportamiento.

Archivo a modificar: src/seguridad/password.py

LONGITUD_MINIMA = 8


def es_password_valido(contrasena):
    return len(contrasena) >= LONGITUD_MINIMA

Ejecutamos python -m pytest. Si todo pasa, el cambio fue seguro.

6.6 Segundo refactor: nombrar mejor la constante

La constante LONGITUD_MINIMA es clara, pero puede ser más específica: se refiere a contraseñas.

Archivo a modificar: src/seguridad/password.py

LONGITUD_MINIMA_PASSWORD = 8


def es_password_valido(contrasena):
    return len(contrasena) >= LONGITUD_MINIMA_PASSWORD

Volvemos a ejecutar python -m pytest. El resultado debe seguir en verde.

6.7 Refactorizar no siempre significa extraer funciones

La función actual tiene una sola línea. Extraer más funciones podría hacerla menos clara, no más clara. Un buen refactor reduce confusión real.

En TDD no refactorizamos por obligación. Refactorizamos cuando el código puede quedar más claro, más simple o menos duplicado.

6.8 Refactor en las pruebas

Las pruebas tienen tres funciones muy parecidas. Podemos usar parametrización para expresar varios ejemplos del mismo comportamiento.

Archivo a modificar: tests/test_password.py

import pytest

from seguridad.password import es_password_valido


@pytest.mark.parametrize(
    "contrasena, esperado",
    [
        ("abcdefgh", True),
        ("abcdefghi", True),
        ("abc", False),
    ],
)
def test_password_se_valida_por_longitud(contrasena, esperado):
    assert es_password_valido(contrasena) is esperado

Ejecutamos python -m pytest. Si la suite pasa, la prueba quedó más compacta sin cambiar lo que verifica.

6.9 Ventaja y costo de parametrizar

La parametrización reduce repetición y facilita agregar ejemplos. Pero también puede ocultar un poco la intención de cada caso si los datos no están bien elegidos.

En este ejemplo funciona bien porque todos los casos prueban la misma regla: validez por longitud mínima.

6.10 Mejorar la lectura de los casos

Podemos agregar identificadores a los casos parametrizados para que el reporte de pytest sea más claro.

Archivo a modificar: tests/test_password.py

import pytest

from seguridad.password import es_password_valido


@pytest.mark.parametrize(
    "contrasena, esperado",
    [
        pytest.param("abcdefgh", True, id="ocho-caracteres"),
        pytest.param("abcdefghi", True, id="mas-de-ocho-caracteres"),
        pytest.param("abc", False, id="menos-de-ocho-caracteres"),
    ],
)
def test_password_se_valida_por_longitud(contrasena, esperado):
    assert es_password_valido(contrasena) is esperado

Ejecutamos nuevamente python -m pytest.

6.11 Evitar refactors que cambian reglas

Este cambio parece pequeño, pero cambia el comportamiento:

def es_password_valido(contrasena):
    return len(contrasena) > LONGITUD_MINIMA_PASSWORD

Usar > en lugar de >= haría inválida una contraseña de 8 caracteres. Eso no es refactor, es un cambio de comportamiento.

6.12 Las pruebas detectan el cambio accidental

Si cometiéramos el error anterior, la prueba del caso "abcdefgh" fallaría. Esa es la utilidad de tener una barra verde antes de refactorizar: cualquier cambio accidental se vuelve visible rápidamente.

Después de detectar el error, restauramos la comparación correcta:

Archivo a modificar: src/seguridad/password.py

LONGITUD_MINIMA_PASSWORD = 8


def es_password_valido(contrasena):
    return len(contrasena) >= LONGITUD_MINIMA_PASSWORD

6.13 Refactor con función auxiliar

Si luego agregamos más reglas, podría ser útil separar la regla de longitud en una función auxiliar. Podemos preparar esa separación sin cambiar comportamiento.

Archivo a modificar: src/seguridad/password.py

LONGITUD_MINIMA_PASSWORD = 8


def tiene_longitud_minima(contrasena):
    return len(contrasena) >= LONGITUD_MINIMA_PASSWORD


def es_password_valido(contrasena):
    return tiene_longitud_minima(contrasena)

Ejecutamos python -m pytest. Si todo sigue en verde, el comportamiento externo no cambió.

6.14 Cuidado con probar funciones auxiliares privadas

Podríamos probar directamente tiene_longitud_minima, pero por ahora la regla ya está cubierta desde es_password_valido. Si probamos cada auxiliar interna, las pruebas pueden volverse frágiles frente a cambios de diseño.

En TDD conviene probar comportamiento público. Las funciones auxiliares pueden cambiar durante futuros refactors.

6.15 Refactor final del código de producción

Con la función auxiliar, el código final queda preparado para crecer con nuevas reglas:

Archivo final: src/seguridad/password.py

LONGITUD_MINIMA_PASSWORD = 8


def tiene_longitud_minima(contrasena):
    return len(contrasena) >= LONGITUD_MINIMA_PASSWORD


def es_password_valido(contrasena):
    return tiene_longitud_minima(contrasena)

La función pública sigue siendo es_password_valido. Las pruebas no necesitan conocer cómo se organiza internamente la regla.

6.16 Refactor final de las pruebas

El archivo de pruebas puede quedar así:

Archivo final: tests/test_password.py

import pytest

from seguridad.password import es_password_valido


@pytest.mark.parametrize(
    "contrasena, esperado",
    [
        pytest.param("abcdefgh", True, id="ocho-caracteres"),
        pytest.param("abcdefghi", True, id="mas-de-ocho-caracteres"),
        pytest.param("abc", False, id="menos-de-ocho-caracteres"),
    ],
)
def test_password_se_valida_por_longitud(contrasena, esperado):
    assert es_password_valido(contrasena) is esperado

Ejecutamos una última vez:

python -m pytest

6.17 Qué cambió y qué no cambió

Cambió la estructura interna: nombres, constante, parametrización y una función auxiliar. No cambió el comportamiento: las contraseñas de 8 o más caracteres siguen siendo válidas y las más cortas siguen siendo inválidas.

Ese es el criterio principal para reconocer una refactorización segura.

6.18 Errores frecuentes al refactorizar

  • Refactorizar con pruebas rojas: primero hay que recuperar la barra verde.
  • Hacer muchos cambios juntos: si algo falla, cuesta encontrar la causa.
  • Agregar comportamiento nuevo: eso requiere una prueba roja nueva, no una refactorización.
  • Modificar pruebas y producción a la vez: conviene cambiar una cosa por vez y ejecutar la suite.
  • Probar detalles internos innecesarios: puede volver frágil la suite.

6.19 Ejercicio propuesto

Refactoriza el nombre de la prueba parametrizada para que sea aún más claro. Por ejemplo, puedes usar:

def test_password_devuelve_resultado_esperado_segun_longitud(contrasena, esperado):
    assert es_password_valido(contrasena) is esperado

Ejecuta python -m pytest. Si todo sigue en verde, el cambio fue seguro. Luego decide cuál nombre comunica mejor el comportamiento.

6.20 Lista de verificación

Antes de continuar, verifica lo siguiente:

  • La suite estaba en verde antes de refactorizar.
  • Realizaste cambios pequeños y verificables.
  • Ejecutaste python -m pytest después de cada cambio importante.
  • Mejoraste nombres o estructura sin cambiar comportamiento.
  • No agregaste nuevas reglas de validación durante el refactor.
  • Comprendes la diferencia entre refactorizar y agregar funcionalidad.

6.21 Conclusión

En este tema refactorizamos después de tener la barra verde. Mejoramos nombres, constantes, estructura interna y pruebas parametrizadas sin cambiar el comportamiento esperado.

En el próximo tema practicaremos baby steps: avanzar con cambios muy pequeños y verificables para mantener el ritmo de TDD sin perder control.