6. Configuración centralizada con pyproject.toml y pytest.ini

6.1 Objetivo del tema

En el tema anterior creamos un script para ejecutar la suite automatizada. Ahora vamos a centralizar la configuración de pytest para no repetir opciones en cada comando ni en cada script.

Veremos dos alternativas habituales: pytest.ini y pyproject.toml. Usaremos ejemplos concretos para configurar carpetas de prueba, nombres de archivos, opciones por defecto y marcadores.

Objetivo práctico: configurar pytest desde un archivo central para que la suite se ejecute siempre con reglas claras y consistentes.

6.2 Por qué centralizar la configuración

Cuando un proyecto crece, empiezan a aparecer comandos largos y repetidos. Por ejemplo:

python -m pytest tests -v --strict-markers --tb=short

Si cada persona escribe el comando de memoria, es fácil que algunas pruebas se ejecuten con opciones distintas. La configuración centralizada evita esa diferencia.

La idea es que este comando simple:

python -m pytest

ya sepa dónde buscar pruebas y qué opciones aplicar.

6.3 Archivos de configuración admitidos por pytest

pytest puede leer configuración desde varios archivos. Los más comunes son:

  • pytest.ini: simple, directo y muy usado en proyectos centrados en pruebas.
  • pyproject.toml: archivo moderno para concentrar configuración de varias herramientas Python.
  • tox.ini: frecuente en proyectos que usan tox.
  • setup.cfg: usado en algunos proyectos antiguos.

En este curso trabajaremos con pytest.ini y pyproject.toml, porque son las opciones más claras para esta etapa.

6.4 Usar pytest.ini

Si quieres una configuración simple, crea o reemplaza el archivo pytest.ini en la raíz del proyecto:

[pytest]
testpaths = tests
python_files = test_*.py
python_functions = test_*
addopts = -v --strict-markers --tb=short
markers =
    lento: pruebas que tardan más tiempo en ejecutarse
    regresion: pruebas importantes para detectar regresiones
    config: pruebas relacionadas con configuración del proyecto

Con esta configuración, pytest buscará pruebas dentro de tests, aplicará salida detallada y validará que los marcadores usados estén declarados.

6.5 Ejecutar con pytest.ini

Después de guardar pytest.ini, ejecuta:

python -m pytest

Aunque el comando es corto, pytest aplicará las opciones definidas en addopts. Por eso no hace falta escribir -v cada vez.

6.6 Entender testpaths

La opción testpaths indica dónde debe buscar pruebas pytest cuando ejecutamos la suite sin indicar una ruta específica.

testpaths = tests

Esto evita que pytest recorra carpetas innecesarias como .venv, reports u otros directorios del proyecto.

6.7 Entender python_files y python_functions

Estas opciones definen convenciones de nombres:

python_files = test_*.py
python_functions = test_*

Con esta configuración, pytest detectará archivos como test_calculadora.py y funciones como test_sumar_devuelve_la_suma.

Mantener una convención evita que una prueba no se ejecute por tener un nombre incorrecto.

6.8 Entender addopts

addopts permite definir opciones que pytest usará automáticamente en cada ejecución.

addopts = -v --strict-markers --tb=short
  • -v: muestra salida detallada.
  • --strict-markers: obliga a declarar los marcadores antes de usarlos.
  • --tb=short: muestra trazas de error más compactas.

6.9 Declarar marcadores

Los marcadores permiten clasificar pruebas. En la configuración declaramos los que usaremos:

markers =
    lento: pruebas que tardan más tiempo en ejecutarse
    regresion: pruebas importantes para detectar regresiones
    config: pruebas relacionadas con configuración del proyecto

Declararlos ayuda a documentar el significado de cada categoría y evita errores de escritura.

6.10 Usar un marcador en una prueba

Modifica tests/test_config.py para marcar las pruebas de configuración:

import pytest

from app.config import obtener_ambiente, obtener_carpeta_reportes, obtener_timeout


@pytest.mark.config
def test_obtener_ambiente_desde_env():
    assert obtener_ambiente() == "local"


@pytest.mark.config
def test_obtener_timeout_desde_env():
    assert obtener_timeout() == 5


@pytest.mark.config
def test_obtener_carpeta_reportes_desde_env():
    assert obtener_carpeta_reportes() == "reports"

Ahora esas pruebas pertenecen al grupo config.

6.11 Ejecutar solo pruebas con un marcador

Para ejecutar solo las pruebas de configuración:

python -m pytest -m config

Para excluir pruebas lentas:

python -m pytest -m "not lento"

Este criterio será muy útil cuando la suite tenga pruebas rápidas, lentas, críticas o de regresión.

6.12 Usar pyproject.toml

Otra forma de configurar pytest es usar pyproject.toml. Este archivo permite concentrar configuración de distintas herramientas Python.

Si decides usar pyproject.toml, puedes escribir:

[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
python_functions = ["test_*"]
addopts = "-v --strict-markers --tb=short"
markers = [
    "lento: pruebas que tardan más tiempo en ejecutarse",
    "regresion: pruebas importantes para detectar regresiones",
    "config: pruebas relacionadas con configuración del proyecto",
]

El contenido es equivalente al ejemplo con pytest.ini, pero usa formato TOML.

6.13 Elegir entre pytest.ini y pyproject.toml

Para este curso, cualquiera de las dos alternativas funciona. Como regla práctica:

  • Usa pytest.ini si quieres una configuración simple y exclusiva de pytest.
  • Usa pyproject.toml si el proyecto ya centraliza allí la configuración de herramientas Python.

No conviene mantener la misma configuración en ambos archivos. Si duplicas opciones, será más difícil saber cuál está usando el proyecto.

Recomendación para el curso: usa pytest.ini en esta etapa para que la configuración sea directa y fácil de leer.

6.14 Ajustar el script run_tests.py

Como pytest.ini ya define -v, podemos simplificar el comando base del script:

def construir_comando(args):
    comando = [sys.executable, "-m", "pytest"]

    if args.rapido:
        comando.extend(["-m", "not lento"])

    if args.detener:
        comando.append("-x")

    if args.reporte:
        REPORTS_DIR.mkdir(exist_ok=True)
        comando.extend([
            "--html=reports/reporte.html",
            "--self-contained-html",
        ])

    return comando

El script queda más limpio porque las opciones generales viven en la configuración centralizada.

6.15 Ver la configuración activa

Para revisar qué configuración está tomando pytest, puedes ejecutar:

python -m pytest --trace-config

La salida es extensa, pero sirve para diagnosticar qué archivo de configuración fue detectado.

6.16 Probar una configuración inválida

Si usas un marcador no declarado y tienes --strict-markers, pytest fallará. Por ejemplo:

import pytest


@pytest.mark.critica
def test_ejemplo():
    assert True

Si critica no está declarado en pytest.ini, pytest informará el problema. Esto es bueno: evita que una prueba quede mal clasificada por un error de escritura.

6.17 Agregar el marcador critica

Para permitir ese marcador, agrégalo a pytest.ini:

markers =
    lento: pruebas que tardan más tiempo en ejecutarse
    regresion: pruebas importantes para detectar regresiones
    config: pruebas relacionadas con configuración del proyecto
    critica: pruebas indispensables para validar el comportamiento principal

Luego puedes ejecutar:

python -m pytest -m critica

6.18 Configuración mínima recomendada

Para continuar el curso, deja este pytest.ini en la raíz:

[pytest]
testpaths = tests
python_files = test_*.py
python_functions = test_*
addopts = -v --strict-markers --tb=short
markers =
    lento: pruebas que tardan más tiempo en ejecutarse
    regresion: pruebas importantes para detectar regresiones
    config: pruebas relacionadas con configuración del proyecto
    critica: pruebas indispensables para validar el comportamiento principal

6.19 Problemas frecuentes

  • pytest no toma la configuración: revisa que estés ejecutando desde la raíz del proyecto.
  • Un marcador produce error: agrégalo en la sección markers.
  • Se ejecutan pruebas que no esperabas: revisa testpaths y las convenciones de nombres.
  • Hay configuración duplicada: usa pytest.ini o pyproject.toml, pero no ambos para lo mismo.
  • El script repite opciones: mueve las opciones generales a addopts.

6.20 Ejercicio práctico

Agrega un marcador llamado texto para identificar pruebas relacionadas con el módulo app/textos.py.

Luego marca las pruebas de tests/test_textos.py con @pytest.mark.texto y ejecuta solo ese grupo:

python -m pytest -m texto

6.21 Solución propuesta

En pytest.ini, agrega:

markers =
    lento: pruebas que tardan más tiempo en ejecutarse
    regresion: pruebas importantes para detectar regresiones
    config: pruebas relacionadas con configuración del proyecto
    critica: pruebas indispensables para validar el comportamiento principal
    texto: pruebas relacionadas con normalización y manejo de textos

En tests/test_textos.py:

import pytest

from app.textos import normalizar_texto


@pytest.mark.texto
def test_normalizar_texto_quita_espacios_externos():
    assert normalizar_texto("  Python  ") == "python"

Finalmente ejecuta:

python -m pytest -m texto

6.22 Lista de verificación

Antes de continuar con el próximo tema, verifica lo siguiente:

  • Existe un archivo pytest.ini o pyproject.toml, pero no ambos con la misma configuración.
  • testpaths apunta a la carpeta tests.
  • Las convenciones de nombres están configuradas.
  • addopts contiene opciones generales de la suite.
  • Los marcadores usados están declarados.
  • La suite se ejecuta correctamente con python -m pytest.
  • El script run_tests.py no duplica opciones innecesarias.

6.23 Conclusión

En este tema centralizamos la configuración de pytest. Esto permite ejecutar la suite con comandos simples y reglas consistentes para todos los alumnos o integrantes de un equipo.

También vimos cómo declarar marcadores, seleccionar grupos de pruebas y elegir entre pytest.ini y pyproject.toml. En el próximo tema trabajaremos con convenciones de nombres para archivos, carpetas, datos y utilidades.