16. Parametrización de pruebas con pytest.mark.parametrize

16.1 Objetivo del tema

Muchas veces necesitamos probar la misma función con varios datos diferentes. Sin parametrización, terminamos escribiendo varias pruebas casi iguales.

pytest.mark.parametrize permite ejecutar una misma función de prueba con distintos valores de entrada y resultados esperados.

Idea clave: parametrizar evita duplicación cuando la estructura de la prueba es la misma y solo cambian los datos.

16.2 Crear una carpeta de práctica

Crea un proyecto nuevo:

mkdir pytest-parametrize-demo
cd pytest-parametrize-demo

Si pytest no está instalado en el entorno activo:

python -m pip install pytest

16.3 Crear el código a probar

Crea un archivo llamado reglas.py:

def es_par(numero):
    return numero % 2 == 0


def calcular_descuento(precio, porcentaje):
    if porcentaje < 0 or porcentaje > 100:
        raise ValueError("El porcentaje debe estar entre 0 y 100")
    return precio - (precio * porcentaje / 100)


def clasificar_edad(edad):
    if edad < 0:
        raise ValueError("La edad no puede ser negativa")
    if edad < 13:
        return "niñez"
    if edad < 18:
        return "adolescencia"
    return "adultez"


def normalizar_nombre(nombre):
    return nombre.strip().title()

Estas funciones tienen varios casos posibles, por eso son buenas candidatas para parametrizar.

16.4 El problema de repetir pruebas

Sin parametrización, podríamos escribir varias pruebas similares:

from reglas import es_par


def test_2_es_par():
    assert es_par(2) is True


def test_3_no_es_par():
    assert es_par(3) is False


def test_10_es_par():
    assert es_par(10) is True

Las pruebas son correctas, pero repiten la misma estructura. Solo cambian los datos.

16.5 Primera prueba parametrizada

Podemos reemplazar esas pruebas con una sola función parametrizada:

import pytest

from reglas import es_par


@pytest.mark.parametrize("numero, esperado", [
    (2, True),
    (3, False),
    (10, True),
])
def test_es_par(numero, esperado):
    assert es_par(numero) is esperado

pytest ejecutará test_es_par una vez por cada tupla de datos.

16.6 Ejecutar la prueba parametrizada

Ejecuta:

python -m pytest -v

La salida mostrará varias ejecuciones de la misma prueba, una por cada caso:

test_reglas.py::test_es_par[2-True] PASSED
test_reglas.py::test_es_par[3-False] PASSED
test_reglas.py::test_es_par[10-True] PASSED

16.7 Parametrizar más de dos valores

Una prueba puede recibir varios parámetros:

from reglas import calcular_descuento


@pytest.mark.parametrize("precio, porcentaje, esperado", [
    (1000, 10, 900),
    (2000, 25, 1500),
    (500, 0, 500),
    (800, 100, 0),
])
def test_calcular_descuento(precio, porcentaje, esperado):
    resultado = calcular_descuento(precio, porcentaje)

    assert resultado == esperado

Los nombres de parámetros en el decorador deben coincidir con los argumentos de la función de prueba.

16.8 Parametrizar cadenas

También podemos parametrizar pruebas sobre texto:

from reglas import normalizar_nombre


@pytest.mark.parametrize("entrada, esperado", [
    (" ana ", "Ana"),
    ("JUAN PEREZ", "Juan Perez"),
    ("  maria  ", "Maria"),
])
def test_normalizar_nombre(entrada, esperado):
    assert normalizar_nombre(entrada) == esperado

16.9 Parametrizar reglas con límites

La función clasificar_edad tiene límites claros. Podemos probarlos en una tabla de casos:

from reglas import clasificar_edad


@pytest.mark.parametrize("edad, esperado", [
    (0, "niñez"),
    (12, "niñez"),
    (13, "adolescencia"),
    (17, "adolescencia"),
    (18, "adultez"),
    (65, "adultez"),
])
def test_clasificar_edad(edad, esperado):
    assert clasificar_edad(edad) == esperado

La parametrización hace visibles los casos límite: 12, 13, 17 y 18.

16.10 Parametrizar excepciones

También podemos parametrizar entradas que deben lanzar error:

@pytest.mark.parametrize("porcentaje", [
    -1,
    101,
    150,
])
def test_descuento_con_porcentaje_invalido_lanza_error(porcentaje):
    with pytest.raises(ValueError):
        calcular_descuento(1000, porcentaje)

Cada valor inválido ejecuta la misma prueba.

16.11 Parametrizar excepción y mensaje

Si queremos verificar el mensaje:

@pytest.mark.parametrize("edad, mensaje", [
    (-1, "La edad no puede ser negativa"),
    (-10, "La edad no puede ser negativa"),
])
def test_edad_negativa_lanza_mensaje(edad, mensaje):
    with pytest.raises(ValueError) as error:
        clasificar_edad(edad)

    assert str(error.value) == mensaje

Esto mantiene una sola estructura de prueba para varios datos inválidos.

16.12 Usar ids para nombres más claros

Podemos agregar ids para que la salida de pytest -v sea más legible:

@pytest.mark.parametrize(
    "edad, esperado",
    [
        (12, "niñez"),
        (13, "adolescencia"),
        (18, "adultez"),
    ],
    ids=["limite-ninez", "inicio-adolescencia", "inicio-adultez"],
)
def test_clasificar_edad_con_ids(edad, esperado):
    assert clasificar_edad(edad) == esperado

Los ids ayudan cuando hay muchos casos y queremos identificar rápido cuál falló.

16.13 Archivo completo de pruebas

Crea test_reglas.py con este contenido:

import pytest

from reglas import calcular_descuento, clasificar_edad, es_par, normalizar_nombre


@pytest.mark.parametrize("numero, esperado", [
    (2, True),
    (3, False),
    (10, True),
])
def test_es_par(numero, esperado):
    assert es_par(numero) is esperado


@pytest.mark.parametrize("precio, porcentaje, esperado", [
    (1000, 10, 900),
    (2000, 25, 1500),
    (500, 0, 500),
    (800, 100, 0),
])
def test_calcular_descuento(precio, porcentaje, esperado):
    resultado = calcular_descuento(precio, porcentaje)
    assert resultado == esperado


@pytest.mark.parametrize("entrada, esperado", [
    (" ana ", "Ana"),
    ("JUAN PEREZ", "Juan Perez"),
    ("  maria  ", "Maria"),
])
def test_normalizar_nombre(entrada, esperado):
    assert normalizar_nombre(entrada) == esperado


@pytest.mark.parametrize("edad, esperado", [
    (0, "niñez"),
    (12, "niñez"),
    (13, "adolescencia"),
    (17, "adolescencia"),
    (18, "adultez"),
    (65, "adultez"),
])
def test_clasificar_edad(edad, esperado):
    assert clasificar_edad(edad) == esperado


@pytest.mark.parametrize("porcentaje", [
    -1,
    101,
    150,
])
def test_descuento_con_porcentaje_invalido_lanza_error(porcentaje):
    with pytest.raises(ValueError):
        calcular_descuento(1000, porcentaje)


@pytest.mark.parametrize("edad, mensaje", [
    (-1, "La edad no puede ser negativa"),
    (-10, "La edad no puede ser negativa"),
])
def test_edad_negativa_lanza_mensaje(edad, mensaje):
    with pytest.raises(ValueError) as error:
        clasificar_edad(edad)

    assert str(error.value) == mensaje

16.14 Ejecutar todas las pruebas

Ejecuta:

python -m pytest

La salida esperada será similar a:

collected 21 items

test_reglas.py .....................                             [100%]

21 passed in 0.04s

Aunque hay pocas funciones de prueba, pytest ejecuta muchos casos gracias a la parametrización.

16.15 Ejecutar con salida detallada

Para ver cada caso parametrizado:

python -m pytest -v

La salida detallada muestra cada combinación como un caso separado.

16.16 Ejecutar solo una prueba parametrizada

Podemos ejecutar toda una función parametrizada:

python -m pytest test_reglas.py::test_calcular_descuento -v

Esto ejecuta todos los casos de test_calcular_descuento.

16.17 Leer fallas parametrizadas

Si un caso falla, pytest indica qué combinación produjo la falla. Por ejemplo:

FAILED test_reglas.py::test_clasificar_edad[13-adolescencia]

Ese nombre permite ubicar rápidamente los datos que hicieron fallar la prueba.

16.18 Cuándo conviene parametrizar

Conviene parametrizar No conviene parametrizar
La prueba repite la misma estructura con distintos datos. Cada caso necesita preparación muy diferente.
Queremos cubrir varios límites de una regla. La prueba se vuelve difícil de leer por demasiados parámetros.
Los casos pueden expresarse como una tabla clara. Los nombres de casos son más importantes que la reducción de líneas.

16.19 Errores frecuentes

  • No importar pytest: el decorador @pytest.mark.parametrize necesita el módulo importado.
  • Desordenar nombres y valores: los nombres del decorador deben coincidir con los argumentos de la función.
  • Agregar demasiados parámetros: puede volver la prueba difícil de leer.
  • Parametrizar casos muy distintos: si cada caso requiere lógica diferente, conviene separar pruebas.
  • No usar casos límite: parametrizar es ideal para probar bordes como 12, 13, 17 y 18.

16.20 Comandos usados en este tema

mkdir pytest-parametrize-demo
cd pytest-parametrize-demo
python -m pip install pytest
python -m pytest
python -m pytest -v
python -m pytest test_reglas.py::test_calcular_descuento -v

16.21 Qué debes recordar de este tema

  • pytest.mark.parametrize ejecuta la misma prueba con distintos datos.
  • Los nombres de parámetros deben coincidir con los argumentos de la función.
  • La parametrización reduce duplicación cuando solo cambian entradas y resultados esperados.
  • También se puede parametrizar casos que lanzan excepciones.
  • ids ayuda a hacer más legible la salida.
  • No conviene parametrizar si la prueba pierde claridad.

16.22 Conclusión

En este tema aprendimos a usar pytest.mark.parametrize para probar varios casos con una sola función de prueba. Esta técnica reduce repetición y permite representar reglas como tablas de datos.

En el próximo tema veremos fixtures, una herramienta central de pytest para preparar datos y objetos reutilizables antes de ejecutar pruebas.