pytest.mark.parametrize ejecuta una función de prueba con distintos argumentos. Cada combinación se convierte en un caso independiente, lo que facilita localizar fallos y evita duplicar preparación y aserciones.

Nombrar los casos importantes

import pytest


@pytest.mark.parametrize(
    ("texto", "esperado"),
    [
        pytest.param("  Python ", "python", id="espacios-y-caja"),
        pytest.param("", "", id="vacio"),
        pytest.param("API", "api", id="sigla"),
    ],
)
def test_normalizar(texto: str, esperado: str) -> None:
    assert texto.strip().lower() == esperado

Los IDs cortos mantienen legible el informe. No incluyas secretos ni objetos enormes. Para excepciones, parametriza entradas inválidas y usa pytest.raises dentro de la prueba, sin mezclar flujos diferentes solo para reducir líneas.

Dos decoradores apilados crean un producto cartesiano. Puede servir para dimensiones independientes, pero también multiplicar casos sin ganar confianza. Enumera las combinaciones cuando solo algunas importen.

La parametrización aporta datos; las fixtures gestionan preparación, dependencias y limpieza. La guía de fixtures, mocks y monkeypatch separa esas responsabilidades. Evita estado global mutable para que un caso no contamine otro.

La documentación oficial de parametrización de pytest, consultada el 22 de julio de 2026, explica parámetros en funciones, fixtures, módulos y generación dinámica.

Empezar por el comportamiento, no por una masa de datos

Un buen conjunto de parámetros describe una regla mediante ejemplos representativos. Empieza con el caso habitual, los límites y un fallo conocido. Cientos de valores arbitrarios alargan el informe sin comprobar necesariamente una propiedad relevante.

def coste_envio(total: int) -> int:
    if total < 0:
        raise ValueError("el total no puede ser negativo")
    return 0 if total >= 100 else 12


@pytest.mark.parametrize(
    ("total", "esperado"),
    [
        pytest.param(0, 12, id="pedido-vacio"),
        pytest.param(99, 12, id="debajo-del-limite"),
        pytest.param(100, 0, id="en-el-limite"),
        pytest.param(101, 0, id="encima-del-limite"),
    ],
)
def test_coste_envio(total: int, esperado: int) -> None:
    assert coste_envio(total) == esperado

El ID debe explicar por qué existe el caso. Un nombre estable del dominio ayuda más que caso-3. Los IDs explícitos aportan valor cuando los datos son opacos, largos o pueden cambiar.

Probar excepciones sin ocultar la aserción

Separa flujos válidos e inválidos cuando sus aserciones sean distintas. El contrato queda visible y la prueba no necesita condicionales.

@pytest.mark.parametrize("total", [-1, -50], ids=["menos-uno", "menos-cincuenta"])
def test_coste_envio_rechaza_total_negativo(total: int) -> None:
    with pytest.raises(ValueError, match="negativo"):
        coste_envio(total)

Comprobar el tipo de excepción es el mínimo. Compara una parte estable del mensaje solo si forma parte de la interfaz. No acoples la prueba al traceback, la puntuación o detalles internos.

Combinar fixtures y parámetros con intención

Los parámetros expresan variaciones de entrada o expectativa. Las fixtures aportan dependencias, preparación y limpieza:

@pytest.mark.parametrize("rol", ["lector", "editor"])
def test_perfil_visible(client, usuario_factory, rol: str) -> None:
    usuario = usuario_factory(rol=rol)
    respuesta = client.get(f"/usuarios/{usuario.id}")
    assert respuesta.status_code == 200

Cuando una fixture debe interpretar el parámetro, usa parametrización indirecta:

@pytest.fixture
def base_datos(request):
    return conectar_para_prueba(engine=request.param)


@pytest.mark.parametrize("base_datos", ["sqlite", "postgres"], indirect=True)
def test_repositorio_guarda_y_lee(base_datos) -> None:
    repositorio = Repositorio(base_datos)
    repositorio.guardar({"id": 1, "nombre": "Ada"})
    assert repositorio.buscar(1)["nombre"] == "Ada"

Usa el modo indirecto con moderación porque el valor adquiere significado en dos lugares. Una fixture normal o una fixture fábrica puede ser más clara para un caso aislado.

Entender los decoradores apilados

Los decoradores apilados generan todas las combinaciones. Dos navegadores y tres idiomas producen seis pruebas:

@pytest.mark.parametrize("idioma", ["en", "es", "pt"])
@pytest.mark.parametrize("navegador", ["chromium", "firefox"])
def test_pagina_inicio(navegador: str, idioma: str) -> None:
    ...

Es apropiado cuando las dimensiones son independientes y todas las combinaciones tienen soporte. Si solo ciertas configuraciones existen en producción, enumera tuplas. Una matriz pequeña e intencional es más rápida y fácil de interpretar.

Marcar casos individuales

pytest.param puede añadir marcas a un caso. Usa xfail para una limitación conocida y documentada, y skip cuando la prueba no pueda ejecutarse en ese entorno.

@pytest.mark.parametrize(
    ("valor", "esperado"),
    [
        ("10", 10),
        pytest.param(
            "١٠",
            10,
            marks=pytest.mark.xfail(reason="digitos arabes aun no soportados"),
            id="digitos-arabes",
        ),
    ],
)
def test_convertir_numero(valor: str, esperado: int) -> None:
    assert convertir_numero(valor) == esperado

No conviertas xfail en un destino permanente para fallos sin explicar. Vincula la limitación con trabajo rastreable, escribe una razón específica y considera el modo estricto para detectar una aprobación inesperada.

Generar casos solo cuando la colección lo necesita

pytestmark parametriza un módulo o clase. El hook pytest_generate_tests construye casos durante la colección y resulta útil si las implementaciones soportadas vienen de un registro u opción. Una lista fija y corta se entiende mejor en el decorador.

No consultes servicios remotos durante la colección. La suite se vuelve lenta e inestable antes de comenzar. Guarda casos de contrato estables en el repositorio y reserva integraciones externas para una suite explícita.

Evitar valores mutables compartidos

pytest pasa los parámetros tal como están. Si una prueba modifica una lista o diccionario, otro caso puede observar el cambio. Usa valores inmutables o crea una copia:

@pytest.mark.parametrize("datos", [{"items": []}, {"items": ["libro"]}])
def test_agregar_item(datos: dict[str, list[str]]) -> None:
    datos_locales = {"items": datos["items"].copy()}
    datos_locales["items"].append("lapiz")
    assert "lapiz" in datos_locales["items"]

La misma regla se aplica a bases de datos, variables de entorno, relojes y cachés. La parametrización crea elementos separados, pero no revierte estado externo.

Ejecutar y diagnosticar casos

Usa pytest -vv para ver IDs completos, pytest -k limite para seleccionar casos por nombre y pytest --collect-only para inspeccionar la matriz sin ejecutarla. Este último resulta valioso después de apilar decoradores.

Cuando un caso falla, el ID debería señalar la frontera de negocio. La aserción muestra los valores real y esperado. Añadir impresiones a todos los casos rara vez ayuda porque pytest ya ofrece buena introspección.

Lista de revisión

Comprueba que cada fila representa una regla o riesgo, que los IDs sean estables y seguros, y que un fallo no contamine los demás. Revisa que la matriz no haya crecido por accidente, que entradas inválidas exijan la excepción correcta y que las fixtures mantengan la responsabilidad de limpieza.

La parametrización reduce repetición cuando comportamiento y aserción son iguales. Si cada fila necesita ramas, mocks y verificaciones diferentes, divide la prueba. Una duplicación clara puede ser mejor que una tabla compacta que oculta varios comportamientos.

Elegir casos límite con método

Para rangos numéricos, incluye valores justo debajo, en el punto y justo encima del límite. En texto, considera entrada vacía, espacios, Unicode y longitud máxima documentada. En colecciones, distingue vacío, un elemento y varios. Son recordatorios, no una obligación de combinar todas las posibilidades.

Parte del contrato público y de fallos conocidos. Si una entrada no alcanza la función porque una validación anterior la rechaza, pruébala en esa frontera. Repetir valores imposibles en unidades internas añade ruido y acoplamiento a la implementación.

Las herramientas pairwise reducen matrices grandes, mientras las pruebas basadas en propiedades exploran espacios amplios y reducen fallos. parametrize es ideal para ejemplos con nombre que un revisor debe entender individualmente. Los enfoques se complementan.

Mantener legibles los datos

Si una fila se convierte en una tupla larga de booleanos y resultados, crea un objeto inmutable:

from dataclasses import dataclass


@dataclass(frozen=True)
class CasoPrecio:
    nombre: str
    subtotal: int
    miembro: bool
    esperado: int


CASOS = [
    CasoPrecio("normal", 100, False, 100),
    CasoPrecio("descuento-miembro", 100, True, 90),
]


@pytest.mark.parametrize("caso", CASOS, ids=lambda caso: caso.nombre)
def test_precio_final(caso: CasoPrecio) -> None:
    assert precio_final(caso.subtotal, caso.miembro) == caso.esperado

Los campos con nombre evitan errores de orden. Mantén la estructura cerca de las pruebas, salvo que varios módulos compartan de verdad los mismos contratos. Un catálogo global puede convertirse en otro sistema difícil de recorrer.

No pongas secretos, registros reales de clientes ni conjuntos licenciados en parámetros. Usa ejemplos sintéticos mínimos con la forma relevante. Los informes y artefactos de integración continua suelen mostrar la representación de valores.

Conservar el aislamiento de fallos

pytest puede continuar después de que falle un caso. La ventaja desaparece si los casos escriben en el mismo archivo, consumen un iterador común o dependen del orden. Usa tmp_path, objetos frescos mediante fixtures y estado independiente en la base de datos.

No conviertas el orden de decoradores en un contrato de ejecución. Las pruebas deberían pasar con orden aleatorio y ejecución paralela cuando el proyecto lo permita. Si el escenario exige una secuencia real, es una prueba de flujo y no varios casos parametrizados.

Mantén el nombre de la función centrado en el comportamiento. El ID añade el escenario. Un informe como test_coste_envio[en-el-limite] es breve, fácil de buscar y muestra la regla y la condición del fallo.