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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
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ó.
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.
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.
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
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.
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.
Antes de continuar, verifica lo siguiente:
python -m pytest después de cada cambio importante.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.