En este tema practicaremos una parte esencial de TDD: escribir la primera prueba fallida a partir de un requisito. La prueba no será un agregado posterior, sino la primera forma concreta de expresar qué debe hacer el programa.
Nos concentraremos en la etapa roja. Es decir, escribiremos una prueba, la ejecutaremos y verificaremos que falle por la razón esperada. La implementación mínima para pasar esa prueba quedará para el próximo tema.
Una especificación ejecutable es una descripción del comportamiento esperado que la computadora puede verificar. En TDD, una prueba automatizada cumple ese rol.
En lugar de dejar el requisito solo como texto, lo convertimos en un ejemplo concreto con entrada, acción y resultado esperado. Al ejecutar la prueba, sabemos si el programa cumple o no esa parte del comportamiento.
Trabajaremos con un validador de contraseñas. El primer requisito será deliberadamente pequeño:
Este requisito todavía no habla de mayúsculas, números, símbolos ni reglas de seguridad avanzadas. En TDD empezamos con un comportamiento pequeño y luego agregamos nuevos requisitos mediante nuevas pruebas.
Antes de escribir código, debemos decidir qué queremos observar desde afuera. En este caso queremos llamar a una función y obtener una respuesta booleana.
True si cumple el requisito.La función podría llamarse es_password_valido. Ese nombre comunica una pregunta cuya respuesta será verdadera o falsa.
El primer ejemplo debe ser simple y directo. Una contraseña con 8 caracteres exactos sirve para comprobar el límite mínimo permitido.
Elegir un ejemplo exacto ayuda a evitar ambigüedades. No estamos diciendo todavía qué pasa con 7 caracteres ni con una cadena vacía. Eso lo veremos con otras pruebas.
Usaremos una estructura mínima similar a la del tema anterior:
password-tdd/
|-- src/
| `-- seguridad/
| `-- __init__.py
`-- tests/
El archivo de prueba estará en tests. El código de producción vivirá dentro del paquete seguridad.
Ahora escribimos la prueba. Todavía no creamos password.py ni la función es_password_valido.
Archivo a crear: tests/test_password.py
from seguridad.password import es_password_valido
def test_password_con_ocho_caracteres_es_valido():
resultado = es_password_valido("abcdefgh")
assert resultado is True
Esta prueba expresa el requisito como un ejemplo ejecutable.
Ejecutamos la suite:
python -m pytest
La prueba debe fallar. En TDD eso no significa que hicimos algo mal; significa que acabamos de describir un comportamiento que todavía no existe.
Un fallo posible es:
ModuleNotFoundError: No module named 'seguridad.password'
Este fallo es razonable porque la prueba intenta importar un módulo que todavía no existe. Estamos en rojo por una causa esperada.
La prueba ya tomó varias decisiones de diseño:
seguridad.password.es_password_valido.True.Estas decisiones aparecen antes de implementar la función. Por eso decimos que la prueba guía el diseño.
El nombre test_password_con_ocho_caracteres_es_valido es claro, pero podemos hacerlo un poco más cercano al requisito:
Archivo a modificar: tests/test_password.py
from seguridad.password import es_password_valido
def test_password_es_valido_si_tiene_al_menos_ocho_caracteres():
resultado = es_password_valido("abcdefgh")
assert resultado is True
Un buen nombre de prueba ayuda a entender el comportamiento incluso antes de leer el cuerpo de la prueba.
Podemos escribir la prueba separando preparación, acción y verificación:
Archivo a modificar: tests/test_password.py
from seguridad.password import es_password_valido
def test_password_es_valido_si_tiene_al_menos_ocho_caracteres():
password = "abcdefgh"
resultado = es_password_valido(password)
assert resultado is True
Primero preparamos el dato, luego ejecutamos la acción y finalmente verificamos el resultado.
En pruebas muy pequeñas podríamos escribir todo en una línea:
assert es_password_valido("abcdefgh") is True
Pero al aprender TDD es útil separar los pasos. Las variables hacen visible qué dato estamos usando y qué resultado esperamos obtener.
En este tema nos detenemos en la etapa roja. Aunque ya sepamos cómo implementar la función, todavía no lo haremos.
Esta pausa es importante: antes de pasar a verde debemos estar seguros de que la prueba expresa correctamente el requisito y que el fallo es el esperado.
El requisito dice “al menos 8 caracteres”. Para completar la idea, podemos escribir una prueba para una contraseña demasiado corta.
Archivo a modificar: tests/test_password.py
from seguridad.password import es_password_valido
def test_password_es_valido_si_tiene_al_menos_ocho_caracteres():
password = "abcdefgh"
resultado = es_password_valido(password)
assert resultado is True
def test_password_es_invalido_si_tiene_menos_de_ocho_caracteres():
password = "abc"
resultado = es_password_valido(password)
assert resultado is False
Esta segunda prueba todavía fallará por la misma razón: la función no existe. En el próximo tema implementaremos lo mínimo para empezar a satisfacer estos ejemplos.
Un requisito como “validar una contraseña segura” es demasiado amplio para una primera prueba. Puede incluir longitud, mayúsculas, minúsculas, números, símbolos, palabras prohibidas y otras reglas.
En TDD conviene dividir ese requisito en comportamientos pequeños. Por ejemplo:
Cada regla puede nacer con una prueba roja propia.
Antes de implementar, revisa estas preguntas:
Si alguna respuesta es dudosa, conviene mejorar la prueba antes de escribir producción.
test_password no explica qué se espera.Escribe una prueba roja para el siguiente requisito, pero no implementes todavía el código:
La prueba debería usar es_password_valido("") y esperar False. Después ejecútala con python -m pytest y verifica que siga fallando por una razón esperada.
Antes de continuar, verifica lo siguiente:
python -m pytest.En este tema convertimos un requisito en una especificación ejecutable. Escribimos una prueba antes del código, elegimos ejemplos concretos, leímos el fallo y revisamos si la prueba comunicaba bien el comportamiento esperado.
En el próximo tema usaremos esta prueba roja como punto de partida para escribir el código mínimo necesario y pasar a la etapa verde sin adelantar diseño innecesario.