Fixtures avanzados y mocks en Flask

Intermedio
Flask
Flask
Actualizado: 18/04/2026

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.

Fixtures y mocks en Flask: configuración de pruebas

# 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 - Autor del tutorial

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.