Muchas veces necesitamos probar la misma función con varios datos diferentes. Sin parametrización, terminamos escribiendo varias pruebas casi iguales.
pytest.mark.parametrize permite ejecutar una misma función de prueba con distintos valores de entrada y resultados esperados.
Crea un proyecto nuevo:
mkdir pytest-parametrize-demo
cd pytest-parametrize-demo
Si pytest no está instalado en el entorno activo:
python -m pip install pytest
Crea un archivo llamado reglas.py:
def es_par(numero):
return numero % 2 == 0
def calcular_descuento(precio, porcentaje):
if porcentaje < 0 or porcentaje > 100:
raise ValueError("El porcentaje debe estar entre 0 y 100")
return precio - (precio * porcentaje / 100)
def clasificar_edad(edad):
if edad < 0:
raise ValueError("La edad no puede ser negativa")
if edad < 13:
return "niñez"
if edad < 18:
return "adolescencia"
return "adultez"
def normalizar_nombre(nombre):
return nombre.strip().title()
Estas funciones tienen varios casos posibles, por eso son buenas candidatas para parametrizar.
Sin parametrización, podríamos escribir varias pruebas similares:
from reglas import es_par
def test_2_es_par():
assert es_par(2) is True
def test_3_no_es_par():
assert es_par(3) is False
def test_10_es_par():
assert es_par(10) is True
Las pruebas son correctas, pero repiten la misma estructura. Solo cambian los datos.
Podemos reemplazar esas pruebas con una sola función parametrizada:
import pytest
from reglas import es_par
@pytest.mark.parametrize("numero, esperado", [
(2, True),
(3, False),
(10, True),
])
def test_es_par(numero, esperado):
assert es_par(numero) is esperado
pytest ejecutará test_es_par una vez por cada tupla de datos.
Ejecuta:
python -m pytest -v
La salida mostrará varias ejecuciones de la misma prueba, una por cada caso:
test_reglas.py::test_es_par[2-True] PASSED
test_reglas.py::test_es_par[3-False] PASSED
test_reglas.py::test_es_par[10-True] PASSED
Una prueba puede recibir varios parámetros:
from reglas import calcular_descuento
@pytest.mark.parametrize("precio, porcentaje, esperado", [
(1000, 10, 900),
(2000, 25, 1500),
(500, 0, 500),
(800, 100, 0),
])
def test_calcular_descuento(precio, porcentaje, esperado):
resultado = calcular_descuento(precio, porcentaje)
assert resultado == esperado
Los nombres de parámetros en el decorador deben coincidir con los argumentos de la función de prueba.
También podemos parametrizar pruebas sobre texto:
from reglas import normalizar_nombre
@pytest.mark.parametrize("entrada, esperado", [
(" ana ", "Ana"),
("JUAN PEREZ", "Juan Perez"),
(" maria ", "Maria"),
])
def test_normalizar_nombre(entrada, esperado):
assert normalizar_nombre(entrada) == esperado
La función clasificar_edad tiene límites claros. Podemos probarlos en una tabla de casos:
from reglas import clasificar_edad
@pytest.mark.parametrize("edad, esperado", [
(0, "niñez"),
(12, "niñez"),
(13, "adolescencia"),
(17, "adolescencia"),
(18, "adultez"),
(65, "adultez"),
])
def test_clasificar_edad(edad, esperado):
assert clasificar_edad(edad) == esperado
La parametrización hace visibles los casos límite: 12, 13, 17 y 18.
También podemos parametrizar entradas que deben lanzar error:
@pytest.mark.parametrize("porcentaje", [
-1,
101,
150,
])
def test_descuento_con_porcentaje_invalido_lanza_error(porcentaje):
with pytest.raises(ValueError):
calcular_descuento(1000, porcentaje)
Cada valor inválido ejecuta la misma prueba.
Si queremos verificar el mensaje:
@pytest.mark.parametrize("edad, mensaje", [
(-1, "La edad no puede ser negativa"),
(-10, "La edad no puede ser negativa"),
])
def test_edad_negativa_lanza_mensaje(edad, mensaje):
with pytest.raises(ValueError) as error:
clasificar_edad(edad)
assert str(error.value) == mensaje
Esto mantiene una sola estructura de prueba para varios datos inválidos.
Podemos agregar ids para que la salida de pytest -v sea más legible:
@pytest.mark.parametrize(
"edad, esperado",
[
(12, "niñez"),
(13, "adolescencia"),
(18, "adultez"),
],
ids=["limite-ninez", "inicio-adolescencia", "inicio-adultez"],
)
def test_clasificar_edad_con_ids(edad, esperado):
assert clasificar_edad(edad) == esperado
Los ids ayudan cuando hay muchos casos y queremos identificar rápido cuál falló.
Crea test_reglas.py con este contenido:
import pytest
from reglas import calcular_descuento, clasificar_edad, es_par, normalizar_nombre
@pytest.mark.parametrize("numero, esperado", [
(2, True),
(3, False),
(10, True),
])
def test_es_par(numero, esperado):
assert es_par(numero) is esperado
@pytest.mark.parametrize("precio, porcentaje, esperado", [
(1000, 10, 900),
(2000, 25, 1500),
(500, 0, 500),
(800, 100, 0),
])
def test_calcular_descuento(precio, porcentaje, esperado):
resultado = calcular_descuento(precio, porcentaje)
assert resultado == esperado
@pytest.mark.parametrize("entrada, esperado", [
(" ana ", "Ana"),
("JUAN PEREZ", "Juan Perez"),
(" maria ", "Maria"),
])
def test_normalizar_nombre(entrada, esperado):
assert normalizar_nombre(entrada) == esperado
@pytest.mark.parametrize("edad, esperado", [
(0, "niñez"),
(12, "niñez"),
(13, "adolescencia"),
(17, "adolescencia"),
(18, "adultez"),
(65, "adultez"),
])
def test_clasificar_edad(edad, esperado):
assert clasificar_edad(edad) == esperado
@pytest.mark.parametrize("porcentaje", [
-1,
101,
150,
])
def test_descuento_con_porcentaje_invalido_lanza_error(porcentaje):
with pytest.raises(ValueError):
calcular_descuento(1000, porcentaje)
@pytest.mark.parametrize("edad, mensaje", [
(-1, "La edad no puede ser negativa"),
(-10, "La edad no puede ser negativa"),
])
def test_edad_negativa_lanza_mensaje(edad, mensaje):
with pytest.raises(ValueError) as error:
clasificar_edad(edad)
assert str(error.value) == mensaje
Ejecuta:
python -m pytest
La salida esperada será similar a:
collected 21 items
test_reglas.py ..................... [100%]
21 passed in 0.04s
Aunque hay pocas funciones de prueba, pytest ejecuta muchos casos gracias a la parametrización.
Para ver cada caso parametrizado:
python -m pytest -v
La salida detallada muestra cada combinación como un caso separado.
Podemos ejecutar toda una función parametrizada:
python -m pytest test_reglas.py::test_calcular_descuento -v
Esto ejecuta todos los casos de test_calcular_descuento.
Si un caso falla, pytest indica qué combinación produjo la falla. Por ejemplo:
FAILED test_reglas.py::test_clasificar_edad[13-adolescencia]
Ese nombre permite ubicar rápidamente los datos que hicieron fallar la prueba.
| Conviene parametrizar | No conviene parametrizar |
|---|---|
| La prueba repite la misma estructura con distintos datos. | Cada caso necesita preparación muy diferente. |
| Queremos cubrir varios límites de una regla. | La prueba se vuelve difícil de leer por demasiados parámetros. |
| Los casos pueden expresarse como una tabla clara. | Los nombres de casos son más importantes que la reducción de líneas. |
pytest: el decorador @pytest.mark.parametrize necesita el módulo importado.mkdir pytest-parametrize-demo
cd pytest-parametrize-demo
python -m pip install pytest
python -m pytest
python -m pytest -v
python -m pytest test_reglas.py::test_calcular_descuento -v
pytest.mark.parametrize ejecuta la misma prueba con distintos datos.ids ayuda a hacer más legible la salida.En este tema aprendimos a usar pytest.mark.parametrize para probar varios casos con una sola función de prueba. Esta técnica reduce repetición y permite representar reglas como tablas de datos.
En el próximo tema veremos fixtures, una herramienta central de pytest para preparar datos y objetos reutilizables antes de ejecutar pruebas.