14. Parametrización para automatizar múltiples escenarios con poco código

14.1 Objetivo del tema

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.

Objetivo práctico: reemplazar pruebas repetidas por pruebas parametrizadas claras y fáciles de ampliar.

14.2 Qué es parametrizar una prueba

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

14.3 Crear una función para practicar

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.

14.4 Pruebas repetidas sin parametrización

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.

14.5 Primera prueba parametrizada

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.

14.6 Ejecutar la prueba parametrizada

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.

14.7 Leer una falla parametrizada

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.

14.8 Usar ids para nombres más claros

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.

14.9 Parametrizar casos de error

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.

14.10 Parametrizar textos

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

14.11 Combinar parametrización con marcadores

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.

14.12 Combinar parametrización con fixtures

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.

14.13 Cuidar la legibilidad

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:

  • Misma acción.
  • Misma verificación.
  • Solo cambian datos de entrada y resultado esperado.

14.14 Evitar tablas enormes

Una tabla con demasiados casos puede volverse difícil de mantener. Si hay muchos escenarios, considera agruparlos por intención.

Por ejemplo:

  • Una prueba parametrizada para descuentos válidos.
  • Otra prueba parametrizada para datos inválidos.
  • Otra para límites especiales.

Así cada prueba conserva un propósito claro.

14.15 Parametrizar con diccionarios

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.

14.16 Seleccionar un caso parametrizado

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.

14.17 Documentar escenarios importantes

La tabla de parámetros funciona como documentación ejecutable. Por eso conviene ordenar los casos de forma lógica:

  • Casos comunes primero.
  • Casos borde después.
  • Casos inválidos en una prueba separada.

14.18 Problemas frecuentes

  • La cantidad de nombres no coincide con los valores: revisa la cadena de parámetros y cada tupla.
  • La salida es difícil de leer: agrega ids.
  • La prueba parametrizada hace demasiadas cosas: separa los escenarios por intención.
  • Un caso modifica datos compartidos: usa fixtures que entreguen datos nuevos por prueba.
  • No sabes qué caso falló: mejora el nombre de la prueba y los IDs.

14.19 Ejercicio práctico

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.
  • Cadena vacía devuelve False.

14.20 Solución propuesta

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

14.21 Lista de verificación

Antes de continuar con el próximo tema, verifica lo siguiente:

  • Sabes usar @pytest.mark.parametrize.
  • Parametrizas casos que comparten acción y verificación.
  • Separas casos válidos e inválidos cuando mejora la claridad.
  • Usas ids cuando la salida necesita ser más legible.
  • Puedes combinar parametrización con fixtures.
  • Puedes combinar parametrización con marcadores.
  • La suite se ejecuta correctamente con python -m pytest.

14.22 Conclusión

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.