20. Probar validaciones, casos borde y datos inválidos

20.1 Objetivo del tema

Una parte importante del testing consiste en comprobar qué ocurre cuando los datos no son ideales. No alcanza con probar solo el caso feliz: también debemos probar límites, valores vacíos, datos inválidos y errores esperados.

En este tema trabajaremos con validaciones de usuarios y productos, usando pytest, pytest.raises y parametrización.

Idea clave: los errores de validación suelen aparecer en los bordes, no en los casos normales.

20.2 Crear una carpeta de práctica

Crea un proyecto nuevo:

mkdir pytest-validaciones-demo
cd pytest-validaciones-demo

Si pytest no está instalado en el entorno activo:

python -m pip install pytest

20.3 Crear el código a probar

Crea un archivo llamado validadores.py:

def validar_edad(edad):
    if edad < 0:
        raise ValueError("La edad no puede ser negativa")
    if edad > 120:
        raise ValueError("La edad no puede ser mayor que 120")
    return edad


def es_mayor_de_edad(edad):
    validar_edad(edad)
    return edad >= 18


def validar_email(email):
    email = email.strip().lower()
    if not email:
        raise ValueError("El email es obligatorio")
    if "@" not in email:
        raise ValueError("El email debe contener @")
    if email.startswith("@") or email.endswith("@"):
        raise ValueError("El email tiene formato inválido")
    return email


def crear_producto(nombre, precio, stock):
    if not nombre or not nombre.strip():
        raise ValueError("El nombre es obligatorio")
    if precio <= 0:
        raise ValueError("El precio debe ser mayor que cero")
    if stock < 0:
        raise ValueError("El stock no puede ser negativo")

    return {
        "nombre": nombre.strip().title(),
        "precio": precio,
        "stock": stock,
        "disponible": stock > 0,
    }

Estas funciones tienen reglas claras y varios casos borde posibles.

20.4 Probar el caso normal

Crea test_validadores.py:

from validadores import es_mayor_de_edad, validar_email


def test_edad_20_es_mayor_de_edad():
    assert es_mayor_de_edad(20) is True


def test_validar_email_normaliza_texto():
    resultado = validar_email("  ANA@EXAMPLE.COM  ")

    assert resultado == "ana@example.com"

Estos son casos válidos. Sirven como base, pero todavía no cubren bordes ni errores.

20.5 Probar casos borde de edad

Los bordes importantes para mayoría de edad son 17 y 18:

def test_edad_17_no_es_mayor_de_edad():
    assert es_mayor_de_edad(17) is False


def test_edad_18_es_mayor_de_edad():
    assert es_mayor_de_edad(18) is True

Si la regla usa > en lugar de >=, la prueba con 18 detectaría el error.

20.6 Parametrizar casos borde

Podemos escribir esos casos como tabla:

import pytest


@pytest.mark.parametrize("edad, esperado", [
    (0, False),
    (17, False),
    (18, True),
    (120, True),
])
def test_es_mayor_de_edad_con_casos_borde(edad, esperado):
    assert es_mayor_de_edad(edad) is esperado

La parametrización ayuda a ver todos los límites importantes juntos.

20.7 Probar edad inválida

Los valores fuera del rango permitido deben lanzar error:

from validadores import validar_edad


@pytest.mark.parametrize("edad", [
    -1,
    121,
])
def test_validar_edad_invalida_lanza_error(edad):
    with pytest.raises(ValueError):
        validar_edad(edad)

20.8 Probar mensajes de edad inválida

Si el mensaje importa, también podemos parametrizarlo:

@pytest.mark.parametrize("edad, mensaje", [
    (-1, "La edad no puede ser negativa"),
    (121, "La edad no puede ser mayor que 120"),
])
def test_validar_edad_invalida_muestra_mensaje(edad, mensaje):
    with pytest.raises(ValueError) as error:
        validar_edad(edad)

    assert str(error.value) == mensaje

20.9 Probar emails inválidos

Los emails tienen varios casos inválidos:

@pytest.mark.parametrize("email", [
    "",
    "   ",
    "correo-sin-arroba.com",
    "@example.com",
    "ana@",
])
def test_email_invalido_lanza_error(email):
    with pytest.raises(ValueError):
        validar_email(email)

Esto cubre vacío, espacios, ausencia de @ y posiciones inválidas.

20.10 Probar producto válido

Ahora probamos una función que valida varios campos:

from validadores import crear_producto


def test_crear_producto_valido():
    resultado = crear_producto("  teclado  ", 50000, 3)

    assert resultado == {
        "nombre": "Teclado",
        "precio": 50000,
        "stock": 3,
        "disponible": True,
    }

20.11 Probar stock cero como caso borde

Stock cero no es inválido, pero cambia la disponibilidad:

def test_producto_con_stock_cero_no_esta_disponible():
    resultado = crear_producto("mouse", 12000, 0)

    assert resultado["disponible"] is False

Este es un caso borde: no debe lanzar error, pero produce un estado particular.

20.12 Probar productos inválidos

Podemos parametrizar combinaciones inválidas:

@pytest.mark.parametrize("nombre, precio, stock", [
    ("", 1000, 1),
    ("   ", 1000, 1),
    ("teclado", 0, 1),
    ("teclado", -100, 1),
    ("teclado", 1000, -1),
])
def test_crear_producto_invalido_lanza_error(nombre, precio, stock):
    with pytest.raises(ValueError):
        crear_producto(nombre, precio, stock)

La prueba cubre nombre vacío, precio no positivo y stock negativo.

20.13 Archivo completo de pruebas

El archivo test_validadores.py puede quedar así:

import pytest

from validadores import crear_producto, es_mayor_de_edad, validar_edad, validar_email


def test_edad_20_es_mayor_de_edad():
    assert es_mayor_de_edad(20) is True


def test_validar_email_normaliza_texto():
    resultado = validar_email("  ANA@EXAMPLE.COM  ")
    assert resultado == "ana@example.com"


@pytest.mark.parametrize("edad, esperado", [
    (0, False),
    (17, False),
    (18, True),
    (120, True),
])
def test_es_mayor_de_edad_con_casos_borde(edad, esperado):
    assert es_mayor_de_edad(edad) is esperado


@pytest.mark.parametrize("edad", [
    -1,
    121,
])
def test_validar_edad_invalida_lanza_error(edad):
    with pytest.raises(ValueError):
        validar_edad(edad)


@pytest.mark.parametrize("edad, mensaje", [
    (-1, "La edad no puede ser negativa"),
    (121, "La edad no puede ser mayor que 120"),
])
def test_validar_edad_invalida_muestra_mensaje(edad, mensaje):
    with pytest.raises(ValueError) as error:
        validar_edad(edad)

    assert str(error.value) == mensaje


@pytest.mark.parametrize("email", [
    "",
    "   ",
    "correo-sin-arroba.com",
    "@example.com",
    "ana@",
])
def test_email_invalido_lanza_error(email):
    with pytest.raises(ValueError):
        validar_email(email)


def test_crear_producto_valido():
    resultado = crear_producto("  teclado  ", 50000, 3)
    assert resultado == {
        "nombre": "Teclado",
        "precio": 50000,
        "stock": 3,
        "disponible": True,
    }


def test_producto_con_stock_cero_no_esta_disponible():
    resultado = crear_producto("mouse", 12000, 0)
    assert resultado["disponible"] is False


@pytest.mark.parametrize("nombre, precio, stock", [
    ("", 1000, 1),
    ("   ", 1000, 1),
    ("teclado", 0, 1),
    ("teclado", -100, 1),
    ("teclado", 1000, -1),
])
def test_crear_producto_invalido_lanza_error(nombre, precio, stock):
    with pytest.raises(ValueError):
        crear_producto(nombre, precio, stock)

20.14 Ejecutar las pruebas

Ejecuta:

python -m pytest

La salida esperada será similar a:

collected 22 items

test_validadores.py ......................                       [100%]

22 passed in 0.04s

20.15 Ejecutar con salida detallada

Para ver cada caso parametrizado:

python -m pytest -v

Esto ayuda a identificar qué dato inválido produjo una falla.

20.16 Clasificar tipos de casos

Tipo de caso Ejemplo Qué buscamos
Normal edad = 20 Confirmar el comportamiento esperado más común.
Borde edad = 17, edad = 18 Detectar errores en límites de reglas.
Vacío email = "" Verificar campos obligatorios.
Inválido precio = -100 Confirmar que se lanza un error.

20.17 Elegir pocos casos, pero importantes

No hace falta probar todos los números posibles. Para una regla de mayoría de edad, suelen ser más útiles estos valores:

17
18
120
-1
121

Estos casos cubren límite inferior, límite de la regla, máximo permitido y valores fuera de rango.

20.18 Probar mensajes solo cuando importan

Verificar mensajes de error puede ser útil, pero también hace que la prueba sea más sensible a cambios de texto:

assert str(error.value) == "La edad no puede ser negativa"

Usa esta comprobación cuando el mensaje forma parte del contrato del sistema o será leído por otra capa de la aplicación.

20.19 Errores frecuentes

  • Probar solo casos válidos: deja sin cubrir las reglas de validación.
  • Olvidar los bordes: muchas fallas aparecen en valores como 17, 18, 0 o 120.
  • Confundir inválido con borde: stock cero puede ser válido aunque cambie la disponibilidad.
  • Crear demasiados casos redundantes: muchos datos similares no siempre agregan cobertura real.
  • No comprobar el tipo de excepción: una excepción inesperada puede ocultar un error distinto.

20.20 Comandos usados en este tema

mkdir pytest-validaciones-demo
cd pytest-validaciones-demo
python -m pip install pytest
python -m pytest
python -m pytest -v
python -m pytest test_validadores.py::test_email_invalido_lanza_error -v

20.21 Qué debes recordar de este tema

  • Las validaciones deben probar casos válidos e inválidos.
  • Los casos borde son valores justo alrededor de una regla.
  • Los datos vacíos suelen descubrir errores importantes.
  • pytest.raises verifica excepciones esperadas.
  • La parametrización ayuda a ordenar muchos casos de validación.
  • No todos los bordes son inválidos; algunos son válidos con comportamiento especial.

20.22 Conclusión

En este tema probamos validaciones, casos borde y datos inválidos. Vimos cómo elegir entradas importantes, cómo parametrizar casos y cómo verificar excepciones esperadas.

En el próximo tema trabajaremos con código que usa listas, diccionarios y fechas, estructuras muy frecuentes en aplicaciones Python.