Pytest ofrece un sistema robusto para la gestión del contexto de las pruebas, permitiendo a los desarrolladores definir acciones de configuración (setup) y desconfiguración (teardown) en diferentes niveles de alcance. Estas funciones garantizan que el entorno de prueba se prepare correctamente antes de la ejecución de los casos de prueba y se limpie adecuadamente después, lo cual es fundamental para la fiabilidad y el aislamiento de las pruebas. A continuación, exploraremos los principales niveles de hooks de setup y teardown, con un enfoque en la ejecución a nivel de clase.
Niveles de Alcance de los Hooks en Pytest
- Nivel de Módulo (
setup_module/teardown_module): Se ejecutan una única vez al principio y al final de la ejecución de todas las pruebas contenidas en un archivo de módulo específico. Son ideales para tareas de configuración globales para un conjunto de pruebas. - Nivel de Clase (
setup_class/teardown_class): Estas funciones se ejecutan una vez antes del primer método de prueba y una vez después del último método de prueba dentro de una clase de prueba. Son perfectas para configurar recursos que serán compartidos por todos los métodos de prueba de una clase, como conexiones a bases de datos o instancias de clientes API. - Nivel de Método (
setup_method/teardown_method): Se ejecutan antes y después de cada método de prueba dentro de una clase. Esto asegura que cada prueba tenga un entorno limpio y consistente, ideal para reiniciar estados o datos específicos de cada prueba. - Nivel de Función (
setup_function/teardown_function): Similares a los de nivel de método, pero se aplican a funciones de prueba independientes que no están encapsuladas dentro de una clase. Se ejecutan antes y después de cada función de prueba. - Métodos Legacy (
setup/teardown): Pytest también soporta los métodossetupyteardowntradicionales deunittest, que se ejecutan antes y después de cada método de prueba dentro de una clase. Aunque aún funcionan, se recomienda el uso desetup_method/teardown_methodpara mayor claridad y consistencia con la filosofía de Pytest.
Ejemplo Práctico de Hooks de Pytest
Consideremos el siguiente script de Python, que demuestra la secuencia de ejecución de los diferentes hooks:
import pytest
# Hooks a nivel de función (afectan solo a funciones fuera de clases)
def configurar_funcion_exterior():
print("\n--- [FUNCIÓN EXTERIOR] Configuración antes de cada función de prueba (fuera de clase) ---")
def desmontar_funcion_exterior():
print("--- [FUNCIÓN EXTERIOR] Desmontaje después de cada función de prueba (fuera de clase) ---")
# Hooks a nivel de módulo
def setup_module():
"""
Este hook se ejecuta una vez antes de que cualquier prueba en este módulo se ejecute.
"""
print("\n============== [MÓDULO] Inicio de configuración de módulo ==============")
def teardown_module():
"""
Este hook se ejecuta una vez después de que todas las pruebas en este módulo han finalizado.
"""
print("============== [MÓDULO] Fin de desmontaje de módulo ==============")
# Función de prueba independiente
def test_verificar_substring():
print(" [CASO DE PRUEBA] Ejecutando test_verificar_substring")
sample_text = "Pytest es genial"
assert "genial" in sample_text
class SuiteDePruebasWeb:
"""
Clase que agrupa pruebas relacionadas.
"""
# Hooks legacy de unittest (se ejecutan para cada método de prueba en la clase)
def setup(self):
print(" [LEGACY] Configuración de método de prueba (setup clásico)")
def teardown(self):
print(" [LEGACY] Desmontaje de método de prueba (teardown clásico)")
# Hooks a nivel de clase (se ejecutan una vez por clase)
@classmethod
def setup_class(cls):
print("\n >>>> [CLASE] Configuración antes de todas las pruebas de la clase <<<<")
@classmethod
def teardown_class(cls):
print(" <<<< [CLASE] Desmontaje después de todas las pruebas de la clase >>>>")
# Hooks a nivel de método (se ejecutan para cada método de prueba en la clase)
def setup_method(self):
print(" -- [MÉTODO] Configuración antes de cada método de prueba --")
def teardown_method(self):
print(" -- [MÉTODO] Desmontaje después de cada método de prueba --")
def test_verificar_elemento_presente(self):
print(" [CASO DE PRUEBA] Ejecutando test_verificar_elemento_presente")
element_id = "boton_enviar"
assert element_id is not None # Simula una verificación de elemento
def test_validar_contenido_pagina(self):
print(" [CASO DE PRUEBA] Ejecutando test_validar_contenido_pagina")
page_title = "Página de Inicio"
assert "Inicio" in page_title
# Punto de entrada para ejecutar las pruebas con pytest
if __name__ == '__main__':
pytest.main(['-s', __file__]) # '-s' para mostrar los prints
Resultados de la Ejecución y Orden de Precedencia
Al ejecutar el script anterior con pytest -s, observamos una secuencia de salida que ilustra el orden en que se invocan los diferentes hooks:
============================= test session starts ==============================
platform ... -- Python ..., pytest-..., py-..., pluggy-...
rootdir: ...
collected 3 items
test_hooks_scopes.py
============== [MÓDULO] Inicio de configuración de módulo ==============
--- [FUNCIÓN EXTERIOR] Configuración antes de cada función de prueba (fuera de clase) ---
[CASO DE PRUEBA] Ejecutando test_verificar_substring
.--- [FUNCIÓN EXTERIOR] Desmontaje después de cada función de prueba (fuera de clase) ---
>>>> [CLASE] Configuración antes de todas las pruebas de la clase <<<<
-- [MÉTODO] Configuración antes de cada método de prueba --
[LEGACY] Configuración de método de prueba (setup clásico)
[CASO DE PRUEBA] Ejecutando test_verificar_elemento_presente
[LEGACY] Desmontaje de método de prueba (teardown clásico)
-- [MÉTODO] Desmontaje después de cada método de prueba --
-- [MÉTODO] Configuración antes de cada método de prueba --
[LEGACY] Configuración de método de prueba (setup clásico)
[CASO DE PRUEBA] Ejecutando test_validar_contenido_pagina
[LEGACY] Desmontaje de método de prueba (teardown clásico)
-- [MÉTODO] Desmontaje después de cada método de prueba --
<<<< [CLASE] Desmontaje después de todas las pruebas de la clase >>>>
============== [MÓDULO] Fin de desmontaje de módulo ==============
[100%]
============================== 3 passed in 0.0Xs ===============================
De los resultados se pueden extraer las siguientes conclusiones sobre la precedencia:
- El hook a nivel de módulo (
setup_module/teardown_module) tiene la prioridad más alta, ejecutándose una vez al inicio y al final de todo el módulo. - Los hooks a nivel de función (
setup_function/teardown_function) solo afectan a las funciones de prueba que no están encapsuladas en clases. No enterfieren con los hooks dentro de las clases. - Dentro de una clase, el orden de ejecución para un método de prueba individual es:
setup_class(una vez por clase)setup_method(entes de cada método)setup(legacy, antes de cada método)- Caso de prueba (el método
test_...) teardown(legacy, después de cada método)teardown_method(después de cada método)teardown_class(una vez por clase, al final)
Es importante destacar que setup_module y teardown_module operan de forma independiente de setup_class y teardown_class, y lo mismo ocurre con setup_function y setup_method/setup.
Uso de Fixtures para Configuración Global (conftest.py)
Pytest también permite una gestión de contexto más flexible y reutilizable a través de "fixtures". Estas se definen comúnmente en un archivo conftest.py, que Pytest detecta automáticamente y permite que las fixtures definidas en él sean utilizadas por cualquier prueba en el directorio o subdirectorios.
Un ejemplo de cómo se podría configurar y cerrar un navegador web automáticamente para un conjunto de pruebas, utilizando una fixture con un alcance de sesión y una fixture con autouse=True para el desmontaje:
conftest.py:
import pytest
from selenium import webdriver
@pytest.fixture(scope="session")
def navegador_web():
"""
Fixture que inicializa y devuelve una instancia de WebDriver para Chrome.
El scope="session" asegura que se abra una sola vez para toda la sesión de pruebas.
"""
print("\n[FIXTURE] Abriendo navegador Chrome...")
driver_instance = webdriver.Chrome()
yield driver_instance
print("\n[FIXTURE] Cerrando navegador Chrome al final de la sesión.")
driver_instance.quit()
@pytest.fixture(autouse=True)
def verificar_finalizacion_prueba(request):
"""
Fixture autouse que se ejecuta para cada prueba.
Demuestra cómo se puede inyectar lógica de teardown automática.
"""
yield
print(f" [FIXTURE AUTOUSE] Prueba '{request.node.name}' ha finalizado.")
En este conftest.py:
- La fixture
navegador_webinicializa un navegador Chrome una única vez por sesión de pruebas (gracias ascope="session"). El bloqueyieldpermite ejecutar código antes y después de las pruebas que la utilizan. - La fixture
verificar_finalizacion_pruebaconautouse=Truese ejecuta automáticamente para cada prueba sin necesidad de ser solicitada explícitamente. Es útil para logging o verificaciones generales de finalización.
Este enfoque con fixtures es más potente y flexible que los hooks tradicionales para la gestión de recursos complejos, permitiendo una clara separación de la lógica de configuración y limpieza de los casos de prueba.