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.
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
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.
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.
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.
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.
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)
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
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.
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,
}
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.
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.
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)
Ejecuta:
python -m pytest
La salida esperada será similar a:
collected 22 items
test_validadores.py ...................... [100%]
22 passed in 0.04s
Para ver cada caso parametrizado:
python -m pytest -v
Esto ayuda a identificar qué dato inválido produjo una falla.
| 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. |
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.
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.
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
pytest.raises verifica excepciones esperadas.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.