En este tema construiremos un validador de datos aplicando TDD. El validador no se limitará a responder verdadero o falso: acumulará los errores encontrados para que el usuario pueda corregir todos los problemas en una sola vez.
El ejemplo será un formulario de registro con reglas para nombre, correo electrónico, contraseña y edad.
Empezamos con una regla mínima:
Elegimos devolver una lista porque más adelante queremos acumular varios errores.
Escribimos la primera prueba antes de crear el validador.
Archivo a crear: tests/test_registro.py
from registro import validar_registro
def test_datos_validos_no_generan_errores():
datos = {
"nombre": "Ana",
"email": "ana@example.com",
"password": "secreto123",
"edad": 25,
}
errores = validar_registro(datos)
assert errores == []
Ejecutamos python -m pytest. La prueba falla porque todavía no existe el
módulo ni la función.
Creamos la función mínima para pasar la primera prueba.
Archivo a crear: src/registro.py
def validar_registro(datos):
return []
Esta implementación no valida nada todavía, pero satisface el primer comportamiento.
Ejecutamos nuevamente python -m pytest para confirmar el verde.
Agregamos una regla nueva:
Archivo a modificar: tests/test_registro.py
def test_nombre_es_obligatorio():
datos = {
"nombre": "",
"email": "ana@example.com",
"password": "secreto123",
"edad": 25,
}
errores = validar_registro(datos)
assert errores == ["El nombre es obligatorio"]
La prueba debe fallar porque la función todavía devuelve siempre una lista vacía.
Agregamos el primer error de validación.
Archivo a modificar: src/registro.py
def validar_registro(datos):
errores = []
if datos["nombre"] == "":
errores.append("El nombre es obligatorio")
return errores
Ejecutamos la suite completa. Las pruebas de datos válidos y nombre obligatorio deben pasar.
Sumamos una segunda regla acumulativa.
Archivo a modificar: tests/test_registro.py
def test_email_es_obligatorio():
datos = {
"nombre": "Ana",
"email": "",
"password": "secreto123",
"edad": 25,
}
errores = validar_registro(datos)
assert errores == ["El email es obligatorio"]
La nueva prueba marca el siguiente paso mínimo.
Agregamos la validación sin tocar la regla del nombre.
Archivo a modificar: src/registro.py
def validar_registro(datos):
errores = []
if datos["nombre"] == "":
errores.append("El nombre es obligatorio")
if datos["email"] == "":
errores.append("El email es obligatorio")
return errores
Ejecutamos python -m pytest. El verde confirma que las reglas conviven.
Ahora verificamos explícitamente que el validador no se detenga en el primer error.
Archivo a modificar: tests/test_registro.py
def test_acumula_errores_de_nombre_y_email():
datos = {
"nombre": "",
"email": "",
"password": "secreto123",
"edad": 25,
}
errores = validar_registro(datos)
assert errores == [
"El nombre es obligatorio",
"El email es obligatorio",
]
Esta prueba probablemente ya pase con la implementación actual, pero es útil porque protege una regla importante: acumular errores.
Agregamos una validación simple de formato.
Archivo a modificar: tests/test_registro.py
def test_email_debe_tener_formato_valido():
datos = {
"nombre": "Ana",
"email": "ana.example.com",
"password": "secreto123",
"edad": 25,
}
errores = validar_registro(datos)
assert errores == ["El email no tiene un formato válido"]
No buscamos construir un validador completo de correo. Solo implementamos la regla que el requisito actual pide.
Validamos el formato solo cuando el correo fue informado.
Archivo a modificar: src/registro.py
def validar_registro(datos):
errores = []
if datos["nombre"] == "":
errores.append("El nombre es obligatorio")
if datos["email"] == "":
errores.append("El email es obligatorio")
elif "@" not in datos["email"]:
errores.append("El email no tiene un formato válido")
return errores
El uso de elif evita agregar dos errores sobre el correo cuando está vacío.
Sumamos una regla para la contraseña.
Archivo a modificar: tests/test_registro.py
def test_password_debe_tener_al_menos_ocho_caracteres():
datos = {
"nombre": "Ana",
"email": "ana@example.com",
"password": "abc",
"edad": 25,
}
errores = validar_registro(datos)
assert errores == ["La contraseña debe tener al menos 8 caracteres"]
La prueba nueva vuelve a marcar un ciclo rojo.
Agregamos la validación correspondiente.
Archivo a modificar: src/registro.py
def validar_registro(datos):
errores = []
if datos["nombre"] == "":
errores.append("El nombre es obligatorio")
if datos["email"] == "":
errores.append("El email es obligatorio")
elif "@" not in datos["email"]:
errores.append("El email no tiene un formato válido")
if len(datos["password"]) < 8:
errores.append("La contraseña debe tener al menos 8 caracteres")
return errores
Ejecutamos todas las pruebas para asegurarnos de que la nueva regla no afectó las anteriores.
Agregamos una regla para la edad.
Archivo a modificar: tests/test_registro.py
def test_usuario_debe_ser_mayor_de_edad():
datos = {
"nombre": "Ana",
"email": "ana@example.com",
"password": "secreto123",
"edad": 17,
}
errores = validar_registro(datos)
assert errores == ["La persona debe ser mayor de edad"]
Incorporamos la regla de edad.
Archivo a modificar: src/registro.py
def validar_registro(datos):
errores = []
if datos["nombre"] == "":
errores.append("El nombre es obligatorio")
if datos["email"] == "":
errores.append("El email es obligatorio")
elif "@" not in datos["email"]:
errores.append("El email no tiene un formato válido")
if len(datos["password"]) < 8:
errores.append("La contraseña debe tener al menos 8 caracteres")
if datos["edad"] < 18:
errores.append("La persona debe ser mayor de edad")
return errores
Ejecutamos python -m pytest. La suite completa debe estar en verde.
En las pruebas repetimos muchas veces datos válidos. Podemos crear una función auxiliar que devuelva un registro válido y permita sobrescribir campos.
Archivo a modificar: tests/test_registro.py
def datos_validos(**cambios):
datos = {
"nombre": "Ana",
"email": "ana@example.com",
"password": "secreto123",
"edad": 25,
}
datos.update(cambios)
return datos
Este refactor mejora la lectura de las pruebas sin ocultar la regla que cada una quiere comprobar.
Las pruebas quedan más enfocadas.
Archivo a modificar: tests/test_registro.py
def test_nombre_es_obligatorio():
errores = validar_registro(datos_validos(nombre=""))
assert errores == ["El nombre es obligatorio"]
def test_email_debe_tener_formato_valido():
errores = validar_registro(datos_validos(email="ana.example.com"))
assert errores == ["El email no tiene un formato válido"]
def test_usuario_debe_ser_mayor_de_edad():
errores = validar_registro(datos_validos(edad=17))
assert errores == ["La persona debe ser mayor de edad"]
Después de refactorizar pruebas, ejecutamos nuevamente la suite.
El validador ya tiene varias reglas. Podemos separar funciones privadas para mejorar la lectura, manteniendo el comportamiento.
Archivo a modificar: src/registro.py
def validar_registro(datos):
errores = []
validar_nombre(datos, errores)
validar_email(datos, errores)
validar_password(datos, errores)
validar_edad(datos, errores)
return errores
Esta estructura muestra qué reglas se aplican sin mezclar los detalles de cada una.
Implementamos las funciones auxiliares.
Archivo a modificar: src/registro.py
def validar_nombre(datos, errores):
if datos["nombre"] == "":
errores.append("El nombre es obligatorio")
def validar_email(datos, errores):
if datos["email"] == "":
errores.append("El email es obligatorio")
elif "@" not in datos["email"]:
errores.append("El email no tiene un formato válido")
def validar_password(datos, errores):
if len(datos["password"]) < 8:
errores.append("La contraseña debe tener al menos 8 caracteres")
def validar_edad(datos, errores):
if datos["edad"] < 18:
errores.append("La persona debe ser mayor de edad")
Ejecutamos python -m pytest. Al estar en etapa de refactor, no debería cambiar
ningún resultado esperado.
Si aparece el requisito de aceptar diccionarios incompletos, primero escribimos una prueba. Por ejemplo, si falta el nombre, lo tratamos como nombre vacío.
Archivo a modificar: tests/test_registro.py
def test_nombre_faltante_se_toma_como_obligatorio():
datos = datos_validos()
del datos["nombre"]
errores = validar_registro(datos)
assert errores == ["El nombre es obligatorio"]
Esta prueba muestra una decisión explícita del dominio. No asumimos cómo manejar campos faltantes hasta que el requisito lo pide.
Para pasar la prueba, cambiamos la validación del nombre usando get.
Archivo a modificar: src/registro.py
def validar_nombre(datos, errores):
if datos.get("nombre", "") == "":
errores.append("El nombre es obligatorio")
Podríamos aplicar el mismo criterio a otros campos solo cuando existan pruebas que lo justifiquen.
Extendé el validador con TDD.
Un validador con reglas acumulativas es un buen ejemplo para practicar TDD porque cada regla puede incorporarse de manera pequeña y verificable. Las pruebas terminan funcionando como documentación precisa de qué datos son válidos y qué errores deben informarse.
En el próximo tema construiremos un carrito de compras con comportamiento incremental.