Cuando el reporte de cobertura muestra muchos caminos parecidos sin probar, es tentador copiar y pegar pruebas. Eso funciona al principio, pero el archivo de pruebas se vuelve repetitivo y difícil de mantener.
La parametrización permite ejecutar la misma prueba con distintos datos. Es una herramienta muy útil para cubrir casos normales, casos borde y variantes de reglas sin duplicar código.
pytest.mark.parametrize para cubrir más comportamientos con pruebas compactas y legibles.
Crea el archivo src/tienda/tarifas.py:
def calcular_tarifa(zona, peso):
if peso <= 0:
raise ValueError("El peso debe ser mayor que cero")
if zona == "local":
if peso <= 1:
return 1000
return 1500
if zona == "nacional":
if peso <= 1:
return 2500
return 4000
if zona == "internacional":
if peso <= 1:
return 8000
return 12000
raise ValueError("Zona inválida")
La función combina zonas, pesos, límites y errores. Es un buen caso para parametrizar.
Sin parametrización, podríamos escribir muchas pruebas parecidas:
from tienda.tarifas import calcular_tarifa
def test_tarifa_local_liviana():
assert calcular_tarifa("local", 1) == 1000
def test_tarifa_local_pesada():
assert calcular_tarifa("local", 2) == 1500
def test_tarifa_nacional_liviana():
assert calcular_tarifa("nacional", 1) == 2500
def test_tarifa_nacional_pesada():
assert calcular_tarifa("nacional", 2) == 4000
Las pruebas son claras, pero repiten la misma estructura: entrada, llamada y resultado esperado.
Podemos reemplazar esas pruebas por una sola prueba parametrizada:
import pytest
from tienda.tarifas import calcular_tarifa
@pytest.mark.parametrize(
"zona, peso, esperado",
[
("local", 1, 1000),
("local", 2, 1500),
("nacional", 1, 2500),
("nacional", 2, 4000),
],
)
def test_calcular_tarifa(zona, peso, esperado):
assert calcular_tarifa(zona, peso) == esperado
Pytest ejecuta la misma función de prueba una vez por cada fila de datos.
Para cubrir la zona internacional, solo agregamos más filas a la tabla:
@pytest.mark.parametrize(
"zona, peso, esperado",
[
("local", 1, 1000),
("local", 2, 1500),
("nacional", 1, 2500),
("nacional", 2, 4000),
("internacional", 1, 8000),
("internacional", 2, 12000),
],
)
def test_calcular_tarifa(zona, peso, esperado):
assert calcular_tarifa(zona, peso) == esperado
La cobertura aumenta porque se ejecutan más ramas, pero el código de prueba sigue siendo compacto.
Los límites de peso están en 1. Conviene probar justo en el límite y justo después:
@pytest.mark.parametrize(
"zona, peso, esperado",
[
("local", 1, 1000),
("local", 1.01, 1500),
("nacional", 1, 2500),
("nacional", 1.01, 4000),
("internacional", 1, 8000),
("internacional", 1.01, 12000),
],
)
def test_calcular_tarifa_limites_de_peso(zona, peso, esperado):
assert calcular_tarifa(zona, peso) == esperado
Una tabla de casos bien elegida puede cubrir ramas y casos borde al mismo tiempo.
También podemos parametrizar entradas inválidas:
@pytest.mark.parametrize(
"zona, peso",
[
("local", 0),
("local", -1),
("desconocida", 1),
],
)
def test_calcular_tarifa_rechaza_datos_invalidos(zona, peso):
with pytest.raises(ValueError):
calcular_tarifa(zona, peso)
Esta prueba cubre validaciones distintas usando una estructura común.
Conviene mantener separadas las pruebas que esperan un resultado de las que esperan una excepción.
Esta separación mejora la lectura:
Mezclar ambas cosas en una sola parametrización suele volver la prueba más difícil de entender.
Cuando una tabla tiene varios casos, los ids ayudan a entender qué caso falló:
@pytest.mark.parametrize(
"zona, peso, esperado",
[
("local", 1, 1000),
("local", 1.01, 1500),
("internacional", 1, 8000),
("internacional", 1.01, 12000),
],
ids=[
"local-liviano",
"local-pesado",
"internacional-liviano",
"internacional-pesado",
],
)
def test_calcular_tarifa_con_ids(zona, peso, esperado):
assert calcular_tarifa(zona, peso) == esperado
Si falla un caso, pytest muestra el identificador en el nombre de la prueba.
Ejecuta cobertura con ramas para ver el impacto de los casos parametrizados.
En Windows PowerShell:
$env:PYTHONPATH="src"
python -m pytest --cov=src --cov-branch --cov-report=term-missing
En Linux o macOS:
PYTHONPATH=src python -m pytest --cov=src --cov-branch --cov-report=term-missing
La lista de líneas y ramas faltantes debería reducirse al cubrir más combinaciones de zona y peso.
Parametrizar no significa probar todas las combinaciones posibles. Una tabla enorme puede volverse tan difícil de mantener como muchas pruebas duplicadas.
Elige casos que representen:
No toda repetición debe convertirse en una tabla. A veces es mejor tener pruebas separadas si cada caso necesita una explicación propia o una preparación muy distinta.
Como regla práctica: parametriza cuando los casos comparten la misma intención y solo cambian los datos. Si cambia la idea de la prueba, probablemente convenga escribir otra prueba.
En este tema usamos pytest.mark.parametrize para aumentar cobertura sin duplicar código. La clave es elegir casos que representen ramas, límites y reglas importantes.
En el próximo tema vamos a configurar source, omit, include y branch en pyproject.toml.