37. Pruebas parametrizadas

37.1 Introducción

En muchos temas anteriores aparecieron situaciones donde una misma unidad debía probarse con varios datos parecidos: valores límite, datos válidos, datos inválidos, categorías, descuentos, reglas de negocio y validaciones.

Podemos escribir una prueba separada para cada caso, y muchas veces eso es correcto. Pero cuando todas las pruebas tienen la misma estructura y solo cambian los datos de entrada y el resultado esperado, la repetición puede volverse innecesaria.

Las pruebas parametrizadas permiten ejecutar una misma prueba varias veces con diferentes conjuntos de datos. Así reducimos duplicación sin perder claridad, siempre que los casos estén bien elegidos y bien nombrados.

37.2 Qué es una prueba parametrizada

Una prueba parametrizada es una prueba que recibe datos como parámetros. El framework de testing la ejecuta una vez por cada conjunto de valores definido.

La idea general es esta:

Si varias pruebas verifican el mismo comportamiento y solo cambian los datos, podemos convertirlas en una prueba parametrizada.

Por ejemplo, si queremos comprobar varios importes válidos, no necesitamos repetir todo el cuerpo de la prueba. Podemos indicar una lista de importes y ejecutar la misma verificación para cada uno.

37.3 El problema de la repetición

Supongamos que tenemos una función que valida porcentajes entre 0 y 100:

def es_porcentaje_valido(valor):
    return 0 <= valor <= 100

Podríamos escribir varias pruebas separadas:

def test_porcentaje_cero_es_valido():
    assert es_porcentaje_valido(0) is True


def test_porcentaje_cincuenta_es_valido():
    assert es_porcentaje_valido(50) is True


def test_porcentaje_cien_es_valido():
    assert es_porcentaje_valido(100) is True

Estas pruebas son claras, pero repiten la misma estructura. Si tenemos muchos valores similares, la suite puede crecer sin aportar más intención.

37.4 Primer ejemplo con pytest

En pytest, la parametrización se expresa con @pytest.mark.parametrize. Indicamos el nombre del parámetro y la lista de valores que recibirá la prueba.

import pytest


def es_porcentaje_valido(valor):
    return 0 <= valor <= 100


@pytest.mark.parametrize("valor", [0, 50, 100])
def test_porcentajes_validos(valor):
    assert es_porcentaje_valido(valor) is True

pytest ejecutará esta prueba tres veces: una con valor = 0, otra con valor = 50 y otra con valor = 100.

Conceptualmente seguimos teniendo tres verificaciones, pero el código de la prueba se escribe una sola vez.

37.5 Parametrizar entrada y resultado esperado

Muchas pruebas necesitan variar tanto el dato de entrada como el resultado esperado. Para eso se pueden parametrizar varios valores.

import pytest


def es_porcentaje_valido(valor):
    return 0 <= valor <= 100


@pytest.mark.parametrize("valor, esperado", [
    (-1, False),
    (0, True),
    (50, True),
    (100, True),
    (101, False),
])
def test_validar_porcentaje(valor, esperado):
    assert es_porcentaje_valido(valor) is esperado

Esta prueba cubre valores inválidos y válidos en una sola estructura. Los casos elegidos muestran el rango completo y los límites.

37.6 Cuándo conviene parametrizar

La parametrización conviene cuando los casos comparten la misma intención. No debe usarse solo para escribir menos líneas si eso vuelve la prueba más difícil de leer.

Conviene parametrizar cuando:

  • La preparación es igual o casi igual en todos los casos.
  • La acción probada es la misma.
  • La aserción tiene la misma forma.
  • Los datos representan variantes de una misma regla.
  • El fallo de un caso sigue siendo fácil de entender.

Si cada caso necesita una explicación distinta, una preparación diferente o varias aserciones particulares, probablemente sea mejor escribir pruebas separadas.

37.7 Casos válidos e inválidos

Una forma habitual de usar pruebas parametrizadas es separar casos válidos e inválidos. Esto mantiene nombres simples y resultados homogéneos.

import pytest


def es_codigo_valido(codigo):
    return len(codigo) == 4 and codigo.isalnum()


@pytest.mark.parametrize("codigo", ["A123", "AB12", "9999"])
def test_codigos_validos(codigo):
    assert es_codigo_valido(codigo) is True


@pytest.mark.parametrize("codigo", ["A12", "A-12", "", "ABCDE"])
def test_codigos_invalidos(codigo):
    assert es_codigo_valido(codigo) is False

Esta separación comunica bien la intención: un grupo comprueba códigos aceptados y otro comprueba códigos rechazados.

37.8 Parametrizar casos límite

Los valores límite suelen funcionar muy bien con parametrización, porque muchas veces queremos comprobar valores vecinos alrededor de una regla.

import pytest


def puede_registrarse(edad):
    return edad >= 18


@pytest.mark.parametrize("edad, esperado", [
    (17, False),
    (18, True),
    (19, True),
])
def test_limite_de_mayoria_de_edad(edad, esperado):
    assert puede_registrarse(edad) is esperado

La tabla de datos deja visible el borde de la regla. El valor 18 es el límite exacto; 17 y 19 ayudan a detectar comparaciones mal escritas.

37.9 Parametrizar cálculos

Las pruebas parametrizadas también sirven para cálculos donde queremos verificar varias entradas y salidas esperadas.

import pytest


def aplicar_descuento(precio, porcentaje):
    return precio - (precio * porcentaje / 100)


@pytest.mark.parametrize("precio, porcentaje, esperado", [
    (1000, 10, 900),
    (500, 20, 400),
    (250, 0, 250),
])
def test_aplicar_descuento(precio, porcentaje, esperado):
    assert aplicar_descuento(precio, porcentaje) == esperado

La prueba sigue teniendo una sola intención: comprobar que el descuento se calcula correctamente. Los datos muestran distintos escenarios de la misma regla.

37.10 Parametrizar reglas de negocio

Cuando una regla de negocio clasifica, aprueba o rechaza casos similares, la parametrización puede hacer que los escenarios queden concentrados y fáciles de comparar.

import pytest


def categoria_cliente(compras_anuales):
    if compras_anuales >= 50:
        return "oro"
    if compras_anuales >= 20:
        return "plata"
    return "bronce"


@pytest.mark.parametrize("compras, categoria_esperada", [
    (0, "bronce"),
    (19, "bronce"),
    (20, "plata"),
    (49, "plata"),
    (50, "oro"),
])
def test_categoria_cliente_segun_compras_anuales(compras, categoria_esperada):
    assert categoria_cliente(compras) == categoria_esperada

Esta prueba muestra los límites entre categorías. Es más fácil comparar los casos en una tabla que repartirlos en muchas funciones casi idénticas.

37.11 Parametrizar errores esperados

También se pueden parametrizar casos donde esperamos una excepción. Esto es útil cuando muchos datos inválidos deben rechazarse de la misma manera.

import pytest


def validar_cantidad(cantidad):
    if cantidad <= 0:
        raise ValueError("La cantidad debe ser positiva")


@pytest.mark.parametrize("cantidad", [0, -1, -10])
def test_cantidades_no_positivas_generan_error(cantidad):
    with pytest.raises(ValueError):
        validar_cantidad(cantidad)

Todos los casos comparten la misma expectativa: una cantidad no positiva debe generar un error. Por eso tiene sentido agruparlos.

37.12 Parametrizar mensajes o códigos de error

Si la unidad devuelve códigos o mensajes de error diferentes según el dato, podemos parametrizar entrada y error esperado.

import pytest


def validar_usuario(usuario):
    if usuario["nombre"].strip() == "":
        return "nombre_obligatorio"
    if usuario["edad"] < 18:
        return "edad_minima"
    return None


@pytest.mark.parametrize("usuario, error_esperado", [
    ({"nombre": "", "edad": 20}, "nombre_obligatorio"),
    ({"nombre": "Ana", "edad": 17}, "edad_minima"),
    ({"nombre": "Ana", "edad": 20}, None),
])
def test_validar_usuario(usuario, error_esperado):
    assert validar_usuario(usuario) == error_esperado

Este estilo es útil si la función siempre devuelve un único resultado comparable. Si cada caso requiere muchas comprobaciones, la parametrización puede volverse menos clara.

37.13 Identificadores de casos

Cuando una prueba parametrizada falla, el framework debe mostrar qué caso falló. pytest permite agregar identificadores con ids para que el reporte sea más legible.

import pytest


def puede_registrarse(edad):
    return edad >= 18


@pytest.mark.parametrize(
    "edad, esperado",
    [
        (17, False),
        (18, True),
        (19, True),
    ],
    ids=["menor", "limite", "mayor"]
)
def test_puede_registrarse_segun_edad(edad, esperado):
    assert puede_registrarse(edad) is esperado

Los identificadores no cambian la lógica de la prueba, pero ayudan a interpretar el resultado cuando hay muchos casos.

37.14 Usar pytest.param para documentar casos

Otra forma de dar nombre a cada caso es usar pytest.param con un identificador individual.

import pytest


def es_porcentaje_valido(valor):
    return 0 <= valor <= 100


@pytest.mark.parametrize("valor, esperado", [
    pytest.param(-1, False, id="debajo-del-minimo"),
    pytest.param(0, True, id="minimo-incluido"),
    pytest.param(100, True, id="maximo-incluido"),
    pytest.param(101, False, id="encima-del-maximo"),
])
def test_porcentaje_valido(valor, esperado):
    assert es_porcentaje_valido(valor) is esperado

Esto vuelve más expresiva la tabla de casos, especialmente cuando los valores por sí solos no explican completamente la situación.

37.15 Cuándo no conviene parametrizar

Parametrizar no siempre mejora una prueba. A veces reduce líneas, pero también oculta la intención de cada caso.

No conviene parametrizar cuando:

  • Cada caso verifica una regla distinta.
  • La preparación cambia demasiado entre casos.
  • Las aserciones son diferentes.
  • El nombre de una prueba separada explicaría mejor el comportamiento.
  • La tabla de parámetros se vuelve demasiado grande o difícil de leer.
  • El fallo de un caso no permite entender rápidamente qué se rompió.

La parametrización es una herramienta para mejorar la claridad, no una obligación.

37.16 Evitar tablas enormes

Una tabla de parámetros demasiado grande puede volverse tan difícil de mantener como muchas pruebas repetidas. Si hay decenas de casos, conviene revisar si todos aportan información distinta.

Antes de agregar muchos datos, conviene preguntar:

  • ¿Este caso representa una clase de equivalencia nueva?
  • ¿Está cerca de un límite importante?
  • ¿Protege una regla de negocio concreta?
  • ¿Evita una regresión que ya ocurrió?
  • ¿O solo repite una idea ya cubierta?

Una prueba parametrizada debe seguir siendo selectiva. No se trata de probar todos los valores posibles.

37.17 Separar grupos de casos

Si una función tiene muchos escenarios, puede ser mejor crear varias pruebas parametrizadas pequeñas en lugar de una sola tabla enorme.

import pytest


def es_nombre_valido(nombre):
    return len(nombre.strip()) >= 2


@pytest.mark.parametrize("nombre", ["Ana", "Lu", "Carlos"])
def test_nombres_validos(nombre):
    assert es_nombre_valido(nombre) is True


@pytest.mark.parametrize("nombre", ["", " ", "A"])
def test_nombres_invalidos(nombre):
    assert es_nombre_valido(nombre) is False

Separar casos válidos e inválidos mejora la lectura. Además, si falla un grupo, el nombre de la prueba ya ofrece contexto.

37.18 Parametrización y Arrange, Act, Assert

Una prueba parametrizada también debe conservar una estructura clara. Los parámetros no reemplazan la organización de preparación, ejecución y verificación.

import pytest


def calcular_total(precio, impuesto):
    return precio + (precio * impuesto / 100)


@pytest.mark.parametrize("precio, impuesto, esperado", [
    (1000, 21, 1210),
    (500, 10, 550),
])
def test_calcular_total_con_impuesto(precio, impuesto, esperado):
    resultado = calcular_total(precio, impuesto)

    assert resultado == esperado

Aunque la prueba sea corta, sigue existiendo una ejecución clara y una aserción explícita. Esto facilita leer la intención.

37.19 Parametrizar objetos de prueba

Los parámetros no tienen que ser solo números o textos. También pueden ser diccionarios u objetos simples, siempre que la tabla siga siendo legible.

import pytest


def puede_venderse(producto, cantidad):
    return producto["activo"] and producto["stock"] >= cantidad


@pytest.mark.parametrize("producto, cantidad, esperado", [
    ({"activo": True, "stock": 5}, 3, True),
    ({"activo": True, "stock": 2}, 3, False),
    ({"activo": False, "stock": 5}, 3, False),
])
def test_puede_venderse(producto, cantidad, esperado):
    assert puede_venderse(producto, cantidad) is esperado

Cuando los objetos crecen demasiado, conviene usar funciones auxiliares o fixtures. Ese será el tema siguiente.

37.20 Parametrización y nombres claros

El nombre de una prueba parametrizada debe describir la regla general, no un caso particular. Los casos concretos quedan en la tabla de parámetros.

Por ejemplo, este nombre es adecuado:

def test_puede_registrarse_segun_edad(edad, esperado):
    assert puede_registrarse(edad) is esperado

En cambio, un nombre como test_edad_18_puede_registrarse no encaja bien si la prueba también se ejecuta con 17 y 19. Ese nombre corresponde mejor a una prueba individual.

37.21 Tabla de criterios prácticos

Esta tabla resume cuándo parametrizar y cuándo preferir pruebas separadas.

Situación Conviene Motivo
Misma regla, varios valores límite. Parametrizar. Los datos se comparan mejor en una tabla.
Misma función, aserción idéntica, muchos datos válidos. Parametrizar. Reduce repetición sin perder intención.
Cada caso tiene una explicación distinta. Pruebas separadas. El nombre individual comunica mejor.
Preparación diferente en cada caso. Pruebas separadas o fixtures. Una tabla puede volverse confusa.
Tabla con demasiados casos parecidos. Revisar selección. Puede haber casos redundantes.

37.22 Errores frecuentes con pruebas parametrizadas

Al usar parametrización, conviene evitar estos errores:

  • Parametrizar casos que no tienen la misma intención.
  • Crear tablas grandes con datos redundantes.
  • Usar nombres de prueba que describen solo un caso particular.
  • Mezclar casos válidos, inválidos y errores complejos sin orden claro.
  • Ocultar reglas importantes en datos difíciles de interpretar.
  • Calcular el resultado esperado con la misma lógica que la unidad probada.
  • No usar identificadores cuando los casos son difíciles de distinguir.

Una parametrización útil debe hacer la prueba más simple de leer, no solamente más corta.

37.23 Qué debes recordar de este tema

  • Una prueba parametrizada ejecuta la misma prueba con varios conjuntos de datos.
  • Conviene parametrizar cuando los casos tienen la misma estructura e intención.
  • Los valores límite, clases de equivalencia y reglas repetidas son buenos candidatos.
  • El nombre de la prueba debe describir la regla general.
  • Los datos esperados deben escribirse de forma explícita, no calcularse copiando la implementación.
  • Las tablas de parámetros deben mantenerse pequeñas y significativas.
  • No toda repetición es mala: a veces una prueba separada comunica mejor.

37.24 Conclusión

Las pruebas parametrizadas ayudan a probar una misma regla con varios datos sin repetir innecesariamente el cuerpo de la prueba. Son especialmente útiles para validaciones, valores límite, cálculos, clasificaciones y reglas de negocio con escenarios similares.

La clave es usarlas con criterio. Una buena parametrización deja visibles los casos importantes y mantiene clara la intención de la prueba. Una mala parametrización esconde la regla dentro de una tabla confusa.

En el próximo tema veremos fixtures y preparación reutilizable, una técnica complementaria para organizar datos y objetos que se repiten en varias pruebas.