23. Construcción de un validador de datos con reglas acumulativas

23.1 Objetivo del tema

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.

23.2 Requisito inicial

Empezamos con una regla mínima:

Si todos los datos requeridos son válidos, el validador debe devolver una lista vacía de errores.

Elegimos devolver una lista porque más adelante queremos acumular varios errores.

23.3 Primera prueba: datos válidos

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.

23.4 Implementación mínima

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.

23.5 Segunda regla: nombre obligatorio

Agregamos una regla nueva:

El nombre es obligatorio. Si falta o está vacío, debe agregarse el error "El nombre es obligatorio".

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.

23.6 Implementar nombre obligatorio

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.

23.7 Tercera regla: correo obligatorio

Sumamos una segunda regla acumulativa.

El correo electrónico es obligatorio.

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.

23.8 Implementar correo obligatorio

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.

23.9 Cuarta regla: acumular errores

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.

23.10 Quinta regla: formato de correo

Agregamos una validación simple de formato.

Si el correo no está vacío y no contiene "@", debe agregarse el error "El email no tiene un formato válido".

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.

23.11 Implementar formato de correo

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.

23.12 Sexta regla: contraseña mínima

Sumamos una regla para la contraseña.

La contraseña debe tener al menos 8 caracteres.

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.

23.13 Implementar longitud de contraseña

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.

23.14 Séptima regla: mayoría de edad

Agregamos una regla para la edad.

La persona debe tener 18 años o más.

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"]

23.15 Implementar mayoría 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.

23.16 Refactor: evitar repetir datos válidos

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.

23.17 Pruebas después del refactor

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.

23.18 Refactor del código de producción

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.

23.19 Funciones de validación

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.

23.20 Validar ausencia de campos

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.

23.21 Ajustar acceso a datos

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.

23.22 Ejercicio práctico

Extendé el validador con TDD.

  1. Agregá una prueba para rechazar contraseñas sin números.
  2. Agregá una prueba para rechazar correos sin punto después de la arroba.
  3. Agregá una prueba para edad faltante.
  4. Implementá cada regla con el mínimo código posible.
  5. Refactorizá solo cuando la suite esté en verde.

23.23 Checklist del tema

  • El validador acumula errores en lugar de detenerse en el primero.
  • Cada regla nueva apareció primero como prueba.
  • Las pruebas usan datos válidos como base y modifican solo lo relevante.
  • El refactor separa reglas sin cambiar resultados.
  • Los campos faltantes se tratan solo cuando una prueba define el comportamiento.

23.24 Conclusión

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.