En el ecosistema de pytest, los fixtures son componentes fundamentales para preparar el entorno de prueba y mantener el código limpio. Una de las características más potentes de los fixtures es el parámetro scope, que permite definir el ciclo de vida y la frecuencia de ejecución de la inicialización.
Cuando configuramos autouse=True, el fixture se inyecta automáticamente en las pruebas. Sin embargo, ejecutar la inicialización antes de cada caso de prueba no siempre es eficiente, especialmente cuando se trata de configuraciones costosas o globales. Para resolver esto, scope nos permite ejecutar el fixture una sola vez o agrupar su ejecución según el contexto.
Configuración de fixtures con alcance de sesión
Para demostrar esto, crearemos un archivo conftest.py con una lógica de inicialización reestructurada:
import pytest
metrics = {}
@pytest.fixture(scope="session")
def calculate_sum():
"""Calcula la suma de dos valores base."""
return 12 + 18
@pytest.fixture(scope="session")
def calculate_product():
"""Calcula el producto de dos valores base."""
return 12 * 18
@pytest.fixture(scope="session", autouse=True)
def setup_base_metrics():
"""Inicializa las métricas base del sistema."""
print(f"Iniciando setup_base_metrics: {metrics}")
metrics["val_x"] = 12
metrics["val_y"] = 18
metrics["sum_result"] = 30
metrics["prod_result"] = 216
metrics["combined_a"] = 100
metrics["combined_b"] = 200
@pytest.fixture(scope="session", autouse=True)
def setup_derived_metrics(calculate_sum, calculate_product):
"""Calcula e inicializa métricas derivadas usando otros fixtures."""
print(f"Iniciando setup_derived_metrics: {metrics}")
metrics["combined_a"] = calculate_sum * calculate_product
metrics["combined_b"] = calculate_sum + calculate_product
Es crucial notar que setup_derived_metrics depende de calculate_sum y calculate_product. Por lo tanto, todos estos fixtures deben compartir el mismo nivel de alcance (scope="session").
Si omitimos el alcance de sesión en las dependencias, pytest lanzará una excepción ScopeMismatch. Esto ocurre porque un fixture con un alcance más amplio (como session) no puede depender de un fixture con un alcance más restrictivo (como function, que es el valor por defecto). Las dependencias deben existir en el mismo nivel o en un nivel superior.
Ejecución de las pruebas
A continuación, creamos el archivo de pruebas test_metrics_scope.py:
from conftest import metrics
def test_base_metrics_validation():
# Act
print(f"val_x={metrics['val_x']}")
print(f"val_y={metrics['val_y']}")
print(f"sum_result={metrics['sum_result']}")
print(f"prod_result={metrics['prod_result']}")
# Assert
assert metrics['sum_result'] == metrics['val_x'] + metrics['val_y']
def test_derived_metrics_validation():
print(f"combined_a={metrics['combined_a']}")
print(f"combined_b={metrics['combined_b']}")
# Assert
assert metrics['combined_a'] != metrics['combined_b']
Análisis de la salida
Al ejecutar las pruebas con el comando pytest -sv test_metrics_scope.py, observamos el siguiente comportamiento:
$ pytest -sv test_metrics_scope.py
================================== test session starts ===================================
platform linux -- Python 3.11.5, pytest-8.1.1, pluggy-1.4.0
collected 2 items
test_metrics_scope.py::test_base_metrics_validation Iniciando setup_base_metrics: {}
Iniciando setup_derived_metrics: {'val_x': 12, 'val_y': 18, 'sum_result': 30, 'prod_result': 216, 'combined_a': 100, 'combined_b': 200}
val_x=12
val_y=18
sum_result=30
prod_result=216
PASSED
test_metrics_scope.py::test_derived_metrics_validation combined_a=6480
combined_b=246
PASSED
=================================== 2 passed in 0.02s ====================================
Los mensajes de inicialización (Iniciando setup_base_metrics y Iniciando setup_derived_metrics) se imprimen exactamente una vez, confirmando que el alcance de sesión funciona correctamente y evita ejecuciones redundantes.
Por el contrario, si hubiéramos dejado calculate_sum y calculate_product con el alcance por defecto (function), la salida mostraría errores de configuración:
ScopeMismatch: You tried to access the function scoped fixture calculate_sum with a session scoped request object. Requesting fixture stack:
conftest.py:24: def setup_derived_metrics(calculate_sum, calculate_product)
Niveles de alcance disponibles
Los fixtures se instancina al inicio de su contexto definido y se destruyen al finalizar dicho contexto. pytest ofrece cinco niveles de scope:
- function: Es el valor predeterminado. El fixture se crea y se destruye para cada función de prueba individual.
- class: El fixture se mantiene activo durante la ejecución de todos los métodos de prueba dentro de una misma clase y se destruye al finalizar el último método.
- module: El ciclo de vida abarca todo el archivo
.py. Se destruye después de que la última prueba del módulo haya terminado. - package: El fixture persiste a lo largo de todas las pruebas dentro de un directorio (paquete) y se limpia al concluir la última prueba de ese directorio.
- session: El alcance más amplio. El fixture se inicializa una sola vez al comienzo de toda la sesión de pruebas y se destruye cuando todas las pruebas han finalizado.