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.
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:
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.
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.
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.
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.
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:
Si cada caso necesita una explicación distinta, una preparación diferente o varias aserciones particulares, probablemente sea mejor escribir pruebas separadas.
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.
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.
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.
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.
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.
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.
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.
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.
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:
La parametrización es una herramienta para mejorar la claridad, no una obligación.
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:
Una prueba parametrizada debe seguir siendo selectiva. No se trata de probar todos los valores posibles.
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.
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.
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.
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.
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. |
Al usar parametrización, conviene evitar estos errores:
Una parametrización útil debe hacer la prueba más simple de leer, no solamente más corta.
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.