En este tema aprenderemos a usar pytest.mark.parametrize para expresar varios ejemplos del mismo comportamiento sin duplicar estructura de prueba.
La parametrización es muy útil en TDD cuando distintos datos ejercitan la misma regla. Permite agregar ejemplos rápidamente y mejora la lectura cuando se usa con criterio.
Conviene parametrizar cuando varias pruebas tienen la misma estructura y solo cambian los datos de entrada y el resultado esperado.
Por ejemplo, una función de calificaciones tiene muchos casos con la misma forma:
assert obtener_calificacion(95) == "A"
assert obtener_calificacion(85) == "B"
assert obtener_calificacion(75) == "C"
Sin parametrización, podríamos escribir:
Archivo de ejemplo: tests/test_calificaciones.py
from academico.calificaciones import obtener_calificacion
def test_devuelve_a_para_puntaje_noventa_o_mas():
assert obtener_calificacion(95) == "A"
def test_devuelve_b_para_puntaje_ochenta_o_mas():
assert obtener_calificacion(85) == "B"
def test_devuelve_c_para_puntaje_setenta_o_mas():
assert obtener_calificacion(75) == "C"
Las pruebas son claras, pero repiten la misma estructura.
La versión parametrizada concentra los ejemplos en una tabla:
Archivo a modificar: tests/test_calificaciones.py
import pytest
from academico.calificaciones import obtener_calificacion
@pytest.mark.parametrize(
"puntaje, esperado",
[
(95, "A"),
(85, "B"),
(75, "C"),
],
)
def test_obtener_calificacion_segun_puntaje(puntaje, esperado):
assert obtener_calificacion(puntaje) == esperado
Cada tupla genera un caso de prueba independiente.
Ejecutamos igual que siempre:
python -m pytest
pytest ejecutará la función una vez por cada fila de datos.
Para agregar un caso, sumamos otra fila:
Archivo a modificar: tests/test_calificaciones.py
@pytest.mark.parametrize(
"puntaje, esperado",
[
(95, "A"),
(85, "B"),
(75, "C"),
(60, "D"),
],
)
def test_obtener_calificacion_segun_puntaje(puntaje, esperado):
assert obtener_calificacion(puntaje) == esperado
Esto facilita ampliar la cobertura de ejemplos sin repetir funciones completas.
Podemos usar pytest.param para agregar nombres descriptivos a cada caso:
@pytest.mark.parametrize(
"puntaje, esperado",
[
pytest.param(95, "A", id="calificacion-a"),
pytest.param(85, "B", id="calificacion-b"),
pytest.param(75, "C", id="calificacion-c"),
pytest.param(60, "D", id="calificacion-d"),
],
)
def test_obtener_calificacion_segun_puntaje(puntaje, esperado):
assert obtener_calificacion(puntaje) == esperado
Los IDs hacen más claro el reporte cuando un caso falla.
Los límites son excelentes candidatos para parametrización:
@pytest.mark.parametrize(
"puntaje, esperado",
[
pytest.param(90, "A", id="limite-a"),
pytest.param(89, "B", id="debajo-de-a"),
pytest.param(80, "B", id="limite-b"),
pytest.param(79, "C", id="debajo-de-b"),
pytest.param(70, "C", id="limite-c"),
pytest.param(69, "D", id="debajo-de-c"),
],
)
def test_obtener_calificacion_respeta_limites(puntaje, esperado):
assert obtener_calificacion(puntaje) == esperado
Esta tabla documenta con precisión los bordes de cada rango.
También podemos parametrizar excepciones esperadas para valores fuera de rango:
@pytest.mark.parametrize("puntaje", [-1, 101])
def test_obtener_calificacion_rechaza_puntajes_fuera_de_rango(puntaje):
with pytest.raises(ValueError):
obtener_calificacion(puntaje)
Ambos casos verifican la misma regla: el puntaje debe estar entre 0 y 100.
Podemos agrupar valores no numéricos:
@pytest.mark.parametrize("puntaje", ["noventa", None, [], {}])
def test_obtener_calificacion_rechaza_puntajes_no_numericos(puntaje):
with pytest.raises(TypeError):
obtener_calificacion(puntaje)
Todos estos datos deberían provocar la misma excepción.
Si distintos datos esperan distintas excepciones, también puede parametrizarse:
@pytest.mark.parametrize(
"puntaje, excepcion",
[
(-1, ValueError),
(101, ValueError),
("noventa", TypeError),
(None, TypeError),
],
)
def test_obtener_calificacion_rechaza_puntajes_invalidos(puntaje, excepcion):
with pytest.raises(excepcion):
obtener_calificacion(puntaje)
Esta forma es compacta, pero puede ser menos explícita que separar reglas distintas en pruebas distintas.
No conviene parametrizar si los casos representan reglas distintas y necesitan nombres separados para entenderse mejor.
Por ejemplo, estas dos reglas pueden merecer pruebas separadas:
ValueError.TypeError.La claridad vale más que reducir líneas.
En TDD suele ser útil empezar con una prueba concreta y luego parametrizar cuando aparecen varios ejemplos similares.
Un flujo posible:
Supongamos una función que aplica descuentos porcentuales:
Archivo a crear: tests/test_descuentos.py
import pytest
from tienda.descuentos import aplicar_descuento
@pytest.mark.parametrize(
"precio, porcentaje, esperado",
[
(100, 10, 90),
(200, 25, 150),
(80, 0, 80),
],
)
def test_aplicar_descuento(precio, porcentaje, esperado):
assert aplicar_descuento(precio, porcentaje) == esperado
Los tres casos prueban la misma regla con datos distintos.
Una implementación suficiente sería:
Archivo a crear: src/tienda/descuentos.py
def aplicar_descuento(precio, porcentaje):
descuento = precio * porcentaje / 100
return precio - descuento
Ejecutamos:
python -m pytest
Podemos mejorar los casos usando pytest.param:
@pytest.mark.parametrize(
"precio, porcentaje, esperado",
[
pytest.param(100, 10, 90, id="diez-por-ciento"),
pytest.param(200, 25, 150, id="veinticinco-por-ciento"),
pytest.param(80, 0, 80, id="sin-descuento"),
],
)
def test_aplicar_descuento(precio, porcentaje, esperado):
assert aplicar_descuento(precio, porcentaje) == esperado
Si un caso falla, el nombre del caso ayuda a diagnosticar.
Una tabla parametrizada demasiado grande puede ser difícil de leer. Si tiene muchos casos, conviene agruparlos por regla o crear pruebas parametrizadas separadas.
Por ejemplo, una prueba para descuentos válidos y otra para porcentajes inválidos puede ser más clara que una sola tabla con todo mezclado.
Refactoriza estas pruebas a una prueba parametrizada:
def test_calcular_puntos_para_100():
assert calcular_puntos(100) == 10
def test_calcular_puntos_para_50():
assert calcular_puntos(50) == 5
def test_calcular_puntos_para_9():
assert calcular_puntos(9) == 0
Agrega IDs descriptivos y ejecuta python -m pytest.
Antes de continuar, verifica lo siguiente:
python -m pytest después del refactor.En este tema usamos parametrización para expresar múltiples ejemplos del mismo comportamiento. Bien aplicada, reduce duplicación y permite agregar casos con poco esfuerzo.
En el próximo tema aplicaremos TDD sobre colecciones: listas, diccionarios y transformaciones de datos.