En este último tema construiremos una suite automatizada completa para un pequeño proyecto Python. La idea es integrar estructura, fixtures, parametrización, datos externos, marcadores, reportes y comandos de ejecución.
El proyecto será simple a propósito: un módulo para calcular descuentos, validar cupones y generar recibos de compra.
Automatizaremos pruebas para estas funcionalidades:
Crea esta estructura de carpetas:
tienda/
├── app/
│ ├── __init__.py
│ ├── descuentos.py
│ ├── cupones.py
│ └── recibos.py
├── tests/
│ ├── data/
│ │ └── descuentos.csv
│ ├── conftest.py
│ ├── test_descuentos.py
│ ├── test_cupones.py
│ └── test_recibos.py
├── pytest.ini
├── pyproject.toml
└── run_tests.py
Crea el archivo app/descuentos.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)
Crea el archivo app/cupones.py:
CUPONES_VALIDOS = {
"DESC10": 10,
"DESC20": 20,
"ENVIOGRATIS": 0,
}
def normalizar_cupon(codigo):
return codigo.strip().upper()
def existe_cupon(codigo):
codigo_normalizado = normalizar_cupon(codigo)
return codigo_normalizado in CUPONES_VALIDOS
def obtener_descuento(codigo):
codigo_normalizado = normalizar_cupon(codigo)
if codigo_normalizado not in CUPONES_VALIDOS:
raise ValueError("Cupón inválido")
return CUPONES_VALIDOS[codigo_normalizado]
Crea el archivo app/recibos.py:
from pathlib import Path
def generar_recibo(nombre_cliente, producto, precio_final):
return (
f"Cliente: {nombre_cliente}\n"
f"Producto: {producto}\n"
f"Total: {precio_final:.2f}\n"
)
def guardar_recibo(ruta, contenido):
ruta = Path(ruta)
ruta.write_text(contenido, encoding="utf-8")
return ruta
Crea el archivo pytest.ini para registrar marcadores y opciones comunes:
[pytest]
testpaths = tests
python_files = test_*.py
python_functions = test_*
addopts = -ra
markers =
rapido: pruebas rápidas de la suite
regresion: pruebas importantes para detectar regresiones
archivos: pruebas que trabajan con archivos temporales
Crea el archivo pyproject.toml con una configuración mínima del proyecto:
[project]
name = "tienda"
version = "1.0.0"
description = "Proyecto de práctica para automatización de pruebas"
requires-python = ">=3.10"
[tool.pytest.ini_options]
minversion = "7.0"
Crea el archivo tests/conftest.py:
import pytest
@pytest.fixture
def cliente():
return {
"nombre": "Ana",
"email": "ana@example.com",
}
@pytest.fixture
def producto():
return {
"nombre": "Teclado",
"precio": 100,
}
@pytest.fixture
def recibo_base(cliente, producto):
return {
"cliente": cliente["nombre"],
"producto": producto["nombre"],
"total": 90,
}
Crea el archivo tests/test_descuentos.py:
import pytest
from app.descuentos import calcular_precio_final
@pytest.mark.rapido
@pytest.mark.parametrize(
"precio, descuento, esperado",
[
(100, 0, 100),
(100, 10, 90),
(100, 100, 0),
(250, 20, 200),
],
ids=["sin_descuento", "diez_por_ciento", "descuento_total", "precio_mayor"],
)
def test_calcular_precio_final_devuelve_total_esperado(precio, descuento, esperado):
assert calcular_precio_final(precio, descuento) == esperado
@pytest.mark.regresion
@pytest.mark.parametrize("precio", [-1, -50])
def test_calcular_precio_final_rechaza_precios_negativos(precio):
with pytest.raises(ValueError, match="precio"):
calcular_precio_final(precio, 10)
@pytest.mark.regresion
@pytest.mark.parametrize("descuento", [-1, 101])
def test_calcular_precio_final_rechaza_descuentos_invalidos(descuento):
with pytest.raises(ValueError, match="descuento"):
calcular_precio_final(100, descuento)
Crea el archivo tests/test_cupones.py:
import pytest
from app.cupones import existe_cupon, obtener_descuento
@pytest.mark.rapido
@pytest.mark.parametrize(
"codigo",
["DESC10", "desc10", " DESC10 "],
ids=["mayusculas", "minusculas", "con_espacios"],
)
def test_existe_cupon_reconoce_codigos_validos(codigo):
assert existe_cupon(codigo) is True
@pytest.mark.rapido
def test_existe_cupon_devuelve_false_si_el_codigo_no_existe():
assert existe_cupon("NOEXISTE") is False
@pytest.mark.regresion
def test_obtener_descuento_devuelve_porcentaje_del_cupon():
assert obtener_descuento("DESC20") == 20
@pytest.mark.regresion
def test_obtener_descuento_lanza_error_si_el_cupon_es_invalido():
with pytest.raises(ValueError, match="Cupón inválido"):
obtener_descuento("NOEXISTE")
Crea el archivo tests/test_recibos.py:
import pytest
from app.recibos import generar_recibo, guardar_recibo
@pytest.mark.rapido
def test_generar_recibo_incluye_cliente_producto_y_total(recibo_base):
recibo = generar_recibo(
recibo_base["cliente"],
recibo_base["producto"],
recibo_base["total"],
)
assert "Cliente: Ana" in recibo
assert "Producto: Teclado" in recibo
assert "Total: 90.00" in recibo
@pytest.mark.archivos
def test_guardar_recibo_crea_archivo_temporal(tmp_path):
contenido = generar_recibo("Ana", "Teclado", 90)
ruta = tmp_path / "recibo.txt"
archivo = guardar_recibo(ruta, contenido)
assert archivo.exists()
assert archivo.read_text(encoding="utf-8") == contenido
Crea el archivo tests/data/descuentos.csv:
precio,descuento,esperado
100,0,100
100,10,90
100,100,0
250,20,200
Luego puedes leer esos datos desde una prueba:
import csv
from pathlib import Path
import pytest
from app.descuentos import calcular_precio_final
def cargar_descuentos():
ruta = Path(__file__).parent / "data" / "descuentos.csv"
with ruta.open(encoding="utf-8", newline="") as archivo:
lector = csv.DictReader(archivo)
return [
(float(fila["precio"]), float(fila["descuento"]), float(fila["esperado"]))
for fila in lector
]
@pytest.mark.rapido
@pytest.mark.parametrize("precio, descuento, esperado", cargar_descuentos())
def test_calcular_precio_final_con_datos_csv(precio, descuento, esperado):
assert calcular_precio_final(precio, descuento) == esperado
Desde la terminal, en la raíz del proyecto, ejecuta:
python -m pytest
Si todo está correcto, la suite debe finalizar sin errores.
Para ejecutar solo las pruebas rápidas:
python -m pytest -m rapido
Para ejecutar la regresión:
python -m pytest -m regresion
Para ejecutar las pruebas que trabajan con archivos:
python -m pytest -m archivos
Para ejecutar solo las pruebas de cupones:
python -m pytest tests/test_cupones.py
Para ejecutar pruebas cuyo nombre contiene una palabra:
python -m pytest -k descuento
Crea el archivo run_tests.py:
import subprocess
import sys
COMANDOS = {
"completa": ["python", "-m", "pytest"],
"rapida": ["python", "-m", "pytest", "-m", "rapido"],
"regresion": ["python", "-m", "pytest", "-m", "regresion"],
"archivos": ["python", "-m", "pytest", "-m", "archivos"],
"tiempos": ["python", "-m", "pytest", "--durations=10"],
}
def main():
objetivo = sys.argv[1] if len(sys.argv) > 1 else "completa"
if objetivo not in COMANDOS:
print("Objetivo inválido")
print("Opciones:", ", ".join(COMANDOS))
return 1
resultado = subprocess.run(COMANDOS[objetivo], check=False)
return resultado.returncode
if __name__ == "__main__":
raise SystemExit(main())
Ejemplos de uso:
python run_tests.py completa
python run_tests.py rapida
python run_tests.py regresion
python run_tests.py tiempos
Para obtener un reporte JUnit XML:
python -m pytest --junitxml=reports/resultados.xml
Si instalaste el complemento pytest-html, puedes generar un reporte HTML:
python -m pytest --html=reports/reporte.html --self-contained-html
Cuando una prueba falla, ejecuta primero el archivo más pequeño posible:
python -m pytest tests/test_descuentos.py -vv
Si necesitas ver salidas impresas durante la ejecución:
python -m pytest tests/test_descuentos.py -s
Si quieres revisar las pruebas más lentas:
python -m pytest --durations=10
Antes de considerar finalizada la automatización, revisa:
python -m pytest.tmp_path.Extiende el proyecto con una nueva regla: si el precio final es mayor o igual a 500, el cliente recibe envío gratis.
Implementa una función nueva, por ejemplo calcular_envio(precio_final), y automatiza pruebas para estos escenarios:
Una implementación posible en app/descuentos.py:
def calcular_envio(precio_final):
if precio_final < 0:
raise ValueError("El precio final no puede ser negativo")
if precio_final >= 500:
return 0
return 50
Y sus pruebas:
import pytest
from app.descuentos import calcular_envio
@pytest.mark.regresion
@pytest.mark.parametrize(
"precio_final, esperado",
[
(499, 50),
(500, 0),
(800, 0),
],
ids=["menor_a_quinientos", "igual_a_quinientos", "mayor_a_quinientos"],
)
def test_calcular_envio_devuelve_costo_esperado(precio_final, esperado):
assert calcular_envio(precio_final) == esperado
@pytest.mark.regresion
def test_calcular_envio_rechaza_precio_final_negativo():
with pytest.raises(ValueError, match="precio final"):
calcular_envio(-10)
Ejecuta la regresión:
python -m pytest -m regresion
En este curso construiste una base completa para automatizar pruebas con Python: estructura de proyecto, configuración, ejecución, selección de pruebas, fixtures, parametrización, datos externos, archivos temporales, reportes, diagnóstico y mantenimiento.
El siguiente paso natural es aplicar esta forma de trabajo en proyectos más grandes, combinándola con calidad de código, refactoring, cobertura, pruebas de APIs, pruebas web, CI/CD y rendimiento.