Muchas pruebas tienen la misma estructura y solo cambian los datos de entrada y el resultado esperado. Si copiamos y pegamos una prueba por cada caso, la suite crece demasiado y se vuelve repetitiva.
En este tema usaremos pytest.mark.parametrize para ejecutar una misma prueba con varios escenarios.
Parametrizar significa ejecutar la misma función de prueba varias veces, usando distintos valores.
Por ejemplo, en lugar de escribir tres pruebas para tres descuentos, escribimos una sola prueba con tres escenarios.
import pytest
@pytest.mark.parametrize("precio, descuento, esperado", [
(100, 0, 100),
(100, 10, 90),
(100, 100, 0),
])
def test_aplicar_descuento_devuelve_precio_esperado(precio, descuento, esperado):
assert precio - (precio * descuento / 100) == esperado
Crea el archivo app/precios.py:
def calcular_precio_final(precio, descuento):
if precio < 0:
raise ValueError("El precio no puede ser negativo")
if descuento < 0 or descuento > 100:
raise ValueError("El descuento debe estar entre 0 y 100")
return precio - (precio * descuento / 100)
Esta función nos permitirá automatizar varios escenarios de cálculo.
Sin parametrización, podríamos escribir:
from app.precios import calcular_precio_final
def test_precio_final_sin_descuento():
assert calcular_precio_final(100, 0) == 100
def test_precio_final_con_diez_por_ciento():
assert calcular_precio_final(100, 10) == 90
def test_precio_final_con_descuento_total():
assert calcular_precio_final(100, 100) == 0
Funciona, pero la estructura se repite. Si agregamos muchos casos, el archivo crecerá sin aportar claridad.
Crea tests/test_precios_parametrizados.py:
import pytest
from app.precios import calcular_precio_final
@pytest.mark.parametrize("precio, descuento, esperado", [
(100, 0, 100),
(100, 10, 90),
(100, 100, 0),
])
def test_calcular_precio_final_devuelve_resultado_esperado(precio, descuento, esperado):
assert calcular_precio_final(precio, descuento) == esperado
pytest ejecutará la misma prueba tres veces, una por cada tupla de datos.
Ejecuta:
python -m pytest tests/test_precios_parametrizados.py
Verás que pytest informa varios casos ejecutados aunque solo escribimos una función de prueba.
Si un caso falla, pytest indica qué combinación de parámetros produjo la falla. Esto ayuda a ubicar el escenario exacto.
Por ejemplo, una salida puede mostrar un caso como:
test_calcular_precio_final_devuelve_resultado_esperado[100-10-90]
Ese identificador representa los valores usados para esa ejecución.
Podemos asignar nombres legibles a cada escenario usando ids:
@pytest.mark.parametrize(
"precio, descuento, esperado",
[
(100, 0, 100),
(100, 10, 90),
(100, 100, 0),
],
ids=[
"sin_descuento",
"descuento_diez_por_ciento",
"descuento_total",
],
)
def test_calcular_precio_final_devuelve_resultado_esperado(precio, descuento, esperado):
assert calcular_precio_final(precio, descuento) == esperado
Los IDs hacen que la salida sea más fácil de leer.
También podemos parametrizar entradas inválidas:
@pytest.mark.parametrize("precio, descuento", [
(-1, 10),
(100, -5),
(100, 120),
])
def test_calcular_precio_final_con_datos_invalidos_lanza_value_error(precio, descuento):
with pytest.raises(ValueError):
calcular_precio_final(precio, descuento)
La misma prueba cubre precio negativo, descuento negativo y descuento mayor a 100.
La parametrización también sirve para probar normalización de textos:
import pytest
from app.textos import normalizar_texto
@pytest.mark.parametrize("entrada, esperado", [
(" Python ", "python"),
("PyTest", "pytest"),
("curso de pruebas", "curso de pruebas"),
])
def test_normalizar_texto_devuelve_texto_esperado(entrada, esperado):
assert normalizar_texto(entrada) == esperado
Una prueba parametrizada puede tener marcadores:
import pytest
@pytest.mark.regresion
@pytest.mark.parametrize("entrada, esperado", [
(" Python ", "python"),
("PyTest", "pytest"),
])
def test_normalizar_texto_devuelve_texto_esperado(entrada, esperado):
assert normalizar_texto(entrada) == esperado
Todos los escenarios generados por esa prueba quedan marcados como regresion.
Una prueba parametrizada también puede recibir fixtures:
import pytest
from app.carrito import calcular_total
@pytest.mark.parametrize("cantidad_extra, esperado", [
(0, 350),
(1, 450),
])
def test_calcular_total_con_productos_y_cantidad_extra(productos_carrito, cantidad_extra, esperado):
productos_carrito.append({"precio": 100, "cantidad": cantidad_extra})
assert calcular_total(productos_carrito) == esperado
La fixture prepara datos base y la parametrización agrega variaciones.
No conviene parametrizar una prueba hasta volverla difícil de leer. Si cada caso tiene reglas muy distintas, puede ser mejor escribir pruebas separadas.
Parametriza cuando los casos comparten la misma estructura:
Una tabla con demasiados casos puede volverse difícil de mantener. Si hay muchos escenarios, considera agruparlos por intención.
Por ejemplo:
Así cada prueba conserva un propósito claro.
A veces los casos son más claros usando diccionarios:
@pytest.mark.parametrize("producto, esperado", [
({"precio": 100, "descuento": 0}, 100),
({"precio": 100, "descuento": 10}, 90),
({"precio": 100, "descuento": 100}, 0),
])
def test_aplicar_descuento_devuelve_total_esperado(producto, esperado):
assert aplicar_descuento(producto) == esperado
Esto puede mejorar la lectura cuando hay varios campos relacionados.
Con IDs claros, puedes seleccionar casos por nombre usando -k:
python -m pytest -k descuento_total
Esto refuerza la importancia de usar IDs legibles cuando una prueba parametrizada tiene varios escenarios.
La tabla de parámetros funciona como documentación ejecutable. Por eso conviene ordenar los casos de forma lógica:
ids.Crea una prueba parametrizada para validar_cupon del módulo app/cupones.py. Debe cubrir estos casos:
DESC10 devuelve True.desc10 devuelve True. DESC10 devuelve True.DESC20 devuelve False.False.Archivo tests/test_cupones_parametrizados.py:
import pytest
from app.cupones import validar_cupon
@pytest.mark.parametrize(
"cupon, esperado",
[
("DESC10", True),
("desc10", True),
(" DESC10 ", True),
("DESC20", False),
("", False),
],
ids=[
"codigo_correcto",
"codigo_en_minusculas",
"codigo_con_espacios",
"codigo_incorrecto",
"codigo_vacio",
],
)
def test_validar_cupon_devuelve_resultado_esperado(cupon, esperado):
assert validar_cupon(cupon) is esperado
Ejecuta:
python -m pytest tests/test_cupones_parametrizados.py
Antes de continuar con el próximo tema, verifica lo siguiente:
@pytest.mark.parametrize.ids cuando la salida necesita ser más legible.python -m pytest.En este tema usamos parametrización para automatizar múltiples escenarios con poco código. Esta técnica reduce duplicación y permite agregar casos nuevos de forma ordenada.
En el próximo tema trabajaremos con datos de prueba desde listas, diccionarios, CSV y JSON, ampliando la forma en que alimentamos nuestras pruebas automatizadas.