Fixtures con múltiples ámbitos
Los fixtures en pytest tienen diferentes ámbitos de vida que determinan cuántas veces se crean durante la ejecución de los tests. Elegir el ámbito correcto mejora el rendimiento y el aislamiento.

# tests/conftest.py
import pytest
from app import crear_app
from app.extensions import db as _db
from app.models import Usuario, Producto
@pytest.fixture(scope='session')
def app():
"""Instancia de la app compartida para toda la sesión de tests."""
app = crear_app({
'TESTING': True,
'SQLALCHEMY_DATABASE_URI': 'sqlite:///:memory:',
'WTF_CSRF_ENABLED': False,
'SECRET_KEY': 'test-secret-key',
'MAIL_SUPPRESS_SEND': True,
})
return app
@pytest.fixture(scope='session')
def db(app):
"""Base de datos compartida para la sesión; se crea una vez."""
with app.app_context():
_db.create_all()
yield _db
_db.drop_all()
@pytest.fixture(scope='function', autouse=False)
def session_db(db):
"""
Transacción de base de datos que se revierte tras cada test.
Evita que los tests se contaminen entre sí.
"""
connection = db.engine.connect()
transaction = connection.begin()
yield db
transaction.rollback()
connection.close()
@pytest.fixture(scope='function')
def cliente(app):
"""Cliente HTTP de prueba."""
with app.test_client() as c:
with app.app_context():
yield c
Factory fixtures para datos de prueba
Las factories generan objetos de prueba con datos controlados, facilitando la creación de escenarios complejos:
# tests/factories.py
import pytest
from app.models import Usuario, Producto
from app.extensions import db
from werkzeug.security import generate_password_hash
def crear_usuario_prueba(
nombre='Usuario Test',
email='test@ejemplo.com',
password='Password123!',
rol='usuario',
activo=True
):
"""Factory para crear usuarios de prueba."""
usuario = Usuario(
nombre=nombre,
email=email,
password_hash=generate_password_hash(password),
rol=rol,
activo=activo
)
db.session.add(usuario)
db.session.commit()
return usuario
def crear_producto_prueba(
nombre='Producto Test',
precio=99.99,
stock=10,
categoria='electronica'
):
"""Factory para crear productos de prueba."""
producto = Producto(
nombre=nombre,
precio=precio,
stock=stock,
categoria=categoria
)
db.session.add(producto)
db.session.commit()
return producto
# tests/conftest.py (ampliado)
@pytest.fixture
def usuario_normal(app):
"""Fixture que proporciona un usuario de tipo estándar."""
with app.app_context():
from tests.factories import crear_usuario_prueba
usuario = crear_usuario_prueba(
email='usuario@ejemplo.com',
rol='usuario'
)
yield usuario
from app.extensions import db
db.session.delete(usuario)
db.session.commit()
@pytest.fixture
def usuario_admin(app):
"""Fixture que proporciona un usuario administrador."""
with app.app_context():
from tests.factories import crear_usuario_prueba
admin = crear_usuario_prueba(
nombre='Administrador',
email='admin@ejemplo.com',
rol='admin'
)
yield admin
from app.extensions import db
db.session.delete(admin)
db.session.commit()
Simular sesiones autenticadas
Muchas rutas requieren autenticación. El cliente de prueba permite manipular la sesión directamente:
# tests/test_rutas_autenticadas.py
import pytest
def test_acceso_sin_autenticar(cliente):
"""Las rutas protegidas redirigen a login sin autenticación."""
respuesta = cliente.get('/dashboard')
# Redirige a login
assert respuesta.status_code == 302
assert '/login' in respuesta.headers['Location']
def test_acceso_autenticado(cliente, usuario_normal, app):
"""Un usuario autenticado puede acceder al dashboard."""
with cliente.session_transaction() as sesion:
# Simular sesión de Flask-Login
sesion['_user_id'] = str(usuario_normal.id)
sesion['_fresh'] = True
respuesta = cliente.get('/dashboard')
assert respuesta.status_code == 200
def test_acceso_solo_admin(cliente, usuario_normal, app):
"""Un usuario normal no puede acceder a rutas de administración."""
with cliente.session_transaction() as sesion:
sesion['_user_id'] = str(usuario_normal.id)
sesion['_fresh'] = True
respuesta = cliente.get('/admin/panel')
assert respuesta.status_code == 403
def test_login_correcto(cliente):
"""Verifica que el proceso de login funciona con credenciales válidas."""
import json
respuesta = cliente.post('/api/auth/login', json={
'email': 'usuario@ejemplo.com',
'password': 'Password123!'
})
assert respuesta.status_code == 200
datos = respuesta.get_json()
assert 'token' in datos or 'message' in datos
def test_login_incorrecto(cliente):
"""Verifica que el login falla con credenciales incorrectas."""
respuesta = cliente.post('/api/auth/login', json={
'email': 'usuario@ejemplo.com',
'password': 'clave-incorrecta'
})
assert respuesta.status_code == 401
Mocking con unittest.mock
Los mocks permiten reemplazar dependencias externas (APIs, servicios de correo, almacenamiento) con versiones controladas:
# tests/test_servicios_externos.py
from unittest.mock import patch, MagicMock
import pytest
def test_envio_email_registro(cliente):
"""
Verifica que se intenta enviar un email al registrar un usuario.
Mockea el servicio de correo para no enviar emails reales.
"""
with patch('app.servicios.email.enviar_email') as mock_email:
mock_email.return_value = True
respuesta = cliente.post('/api/usuarios/registrar', json={
'nombre': 'Nuevo Usuario',
'email': 'nuevo@ejemplo.com',
'password': 'Password123!'
})
assert respuesta.status_code == 201
# Verificar que se llamó al servicio de email
mock_email.assert_called_once()
# Verificar los argumentos del email
args = mock_email.call_args
assert 'nuevo@ejemplo.com' in str(args)
def test_api_externa_pago(cliente, usuario_normal, app):
"""Mockea una API de pagos externa."""
with patch('app.servicios.pagos.PasarelaPago.procesar') as mock_pago:
mock_pago.return_value = {
'estado': 'aprobado',
'id_transaccion': 'TXN-12345',
'monto': 99.99
}
with cliente.session_transaction() as sesion:
sesion['_user_id'] = str(usuario_normal.id)
respuesta = cliente.post('/api/pagos/procesar', json={
'monto': 99.99,
'metodo': 'tarjeta',
'token': 'tok_test'
})
assert respuesta.status_code == 200
datos = respuesta.get_json()
assert datos['estado'] == 'aprobado'
mock_pago.assert_called_once_with(
monto=99.99,
token='tok_test'
)
def test_api_externa_falla(cliente, usuario_normal, app):
"""Simula un fallo en el servicio de pagos."""
with patch('app.servicios.pagos.PasarelaPago.procesar') as mock_pago:
mock_pago.side_effect = Exception('Servicio no disponible')
with cliente.session_transaction() as sesion:
sesion['_user_id'] = str(usuario_normal.id)
respuesta = cliente.post('/api/pagos/procesar', json={
'monto': 99.99,
'metodo': 'tarjeta',
'token': 'tok_test'
})
# La aplicación debe manejar el error graciosamente
assert respuesta.status_code == 503
pytest-mock: una alternativa más limpia
pytest-mock proporciona el fixture mocker que simplifica el uso de mocks:
pip install pytest-mock
# tests/test_con_mocker.py
def test_subida_archivo_s3(cliente, mocker, usuario_normal, app):
"""Mockea la subida a S3 usando pytest-mock."""
mock_s3 = mocker.patch('app.servicios.almacenamiento.S3Client.subir')
mock_s3.return_value = 'https://s3.amazonaws.com/bucket/archivo.jpg'
with cliente.session_transaction() as sesion:
sesion['_user_id'] = str(usuario_normal.id)
from io import BytesIO
datos_archivo = {
'archivo': (BytesIO(b'contenido de prueba'), 'imagen.jpg')
}
respuesta = cliente.post(
'/api/archivos/subir',
data=datos_archivo,
content_type='multipart/form-data'
)
assert respuesta.status_code == 200
mock_s3.assert_called_once()
def test_cache_redis(cliente, mocker):
"""Mockea el caché Redis para evitar conexión real."""
mock_redis = mocker.patch('app.extensiones.cache.get')
mock_redis.return_value = None # Simular cache miss
mock_set = mocker.patch('app.extensiones.cache.set')
respuesta = cliente.get('/api/productos/populares')
assert respuesta.status_code == 200
# Verificar que se intentó escribir en cache
mock_set.assert_called_once()
def test_respuesta_desde_cache(cliente, mocker):
"""Simula una respuesta servida desde caché."""
productos_en_cache = [
{'id': 1, 'nombre': 'Laptop', 'precio': 999.99}
]
mock_redis = mocker.patch('app.extensiones.cache.get')
mock_redis.return_value = productos_en_cache
respuesta = cliente.get('/api/productos/populares')
assert respuesta.status_code == 200
datos = respuesta.get_json()
assert len(datos) == 1
Marcar y organizar tests
pytest ofrece marcas para categorizar y filtrar tests:
# tests/test_rendimiento.py
import pytest
@pytest.mark.slow
def test_importacion_masiva(cliente, app):
"""Test lento que importa miles de registros."""
# Este test se puede excluir con: pytest -m "not slow"
pass
@pytest.mark.integration
def test_flujo_completo_compra(cliente, usuario_normal, app):
"""Test de integración del flujo completo de compra."""
# Agregar al carrito
# Procesar pago
# Verificar confirmación de pedido
pass
@pytest.mark.unit
def test_calculo_descuento():
"""Test unitario puro de una función de negocio."""
from app.servicios.precios import calcular_descuento
assert calcular_descuento(100.0, 20) == 80.0
assert calcular_descuento(50.0, 10) == 45.0
# pytest.ini
# [pytest]
# markers =
# slow: tests lentos de integración
# integration: tests de integración completa
# unit: tests unitarios puros
El uso de fixtures bien diseñados y mocks precisos garantiza que los tests sean rápidos, aislados y reflejen con fidelidad el comportamiento real de la aplicación.
Fuentes y referencias
Documentación oficial y recursos externos para profundizar en Flask
Documentación oficial de Flask
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Flask es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de Flask
Explora más contenido relacionado con Flask y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
Crear fixtures reutilizables con diferentes ámbitos de vida. Usar unittest.mock y pytest-mock para aislar dependencias externas. Simular sesiones de usuario autenticadas en tests. Mockear servicios externos como APIs, correo electrónico y almacenamiento. Implementar factories de datos de prueba con faker.