Testing en Flask con pytest

Intermedio
Flask
Flask
Actualizado: 18/04/2026

Configuración de pytest en Flask

Las pruebas son una parte fundamental del desarrollo de software profesional. En Flask, el módulo de testing oficial está basado en el cliente de prueba de Werkzeug y se integra perfectamente con pytest, el framework de testing más popular en el ecosistema Python.

Testing en Flask con pytest: arquitectura de pruebas

Para comenzar, instala las dependencias necesarias:

pip install pytest pytest-flask

La estructura de directorios recomendada para un proyecto Flask con tests es:

mi_proyecto/
├── app/
│   ├── __init__.py
│   └── routes.py
├── tests/
│   ├── conftest.py
│   ├── test_routes.py
│   └── test_modelos.py
├── requirements.txt
└── pytest.ini

El archivo pytest.ini configura el comportamiento de pytest para el proyecto:

[pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*

Fixtures con conftest.py

Los fixtures en pytest permiten reutilizar configuraciones y recursos entre diferentes tests. El archivo conftest.py es el lugar ideal para definir los fixtures compartidos de tu proyecto Flask.

# tests/conftest.py
import pytest
from app import crear_app
from app.extensions import db as _db

@pytest.fixture(scope='session')
def app():
    """Crea una instancia de la aplicación configurada para tests."""
    app = crear_app({
        'TESTING': True,
        'SQLALCHEMY_DATABASE_URI': 'sqlite:///:memory:',
        'WTF_CSRF_ENABLED': False,
        'SECRET_KEY': 'clave-de-prueba-no-usar-en-produccion'
    })
    return app

@pytest.fixture(scope='session')
def db(app):
    """Configura la base de datos de prueba."""
    with app.app_context():
        _db.create_all()
        yield _db
        _db.drop_all()

@pytest.fixture(scope='function')
def cliente(app):
    """Proporciona un cliente de prueba HTTP para cada test."""
    with app.test_client() as cliente:
        with app.app_context():
            yield cliente

@pytest.fixture(scope='function')
def runner(app):
    """Proporciona un runner para comandos CLI de la aplicación."""
    return app.test_cli_runner()

El parámetro scope controla la vida del fixture:

  • sessión: se crea una vez por sesión de tests
  • module: una vez por módulo de test
  • function: una vez por función de test (valor por defecto)

La aplicación de ejemplo para testear

Antes de escribir tests, definamos una aplicación Flask sencilla:

# app/__init__.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()

def crear_app(config=None):
    app = Flask(__name__)

    app.config.from_mapping(
        SECRET_KEY='dev',
        SQLALCHEMY_DATABASE_URI='sqlite:///app.db',
    )

    if config:
        app.config.update(config)

    db.init_app(app)

    from app import rutas
    app.register_blueprint(rutas.bp)

    return app
# app/rutas.py
from flask import Blueprint, jsonify, request, abort

bp = Blueprint('api', __name__)

productos = [
    {'id': 1, 'nombre': 'Laptop', 'precio': 999.99},
    {'id': 2, 'nombre': 'Mouse', 'precio': 29.99},
]

@bp.route('/productos', methods=['GET'])
def listar_productos():
    return jsonify(productos)

@bp.route('/productos/<int:producto_id>', methods=['GET'])
def obtener_producto(producto_id):
    producto = next((p for p in productos if p['id'] == producto_id), None)
    if producto is None:
        abort(404)
    return jsonify(producto)

@bp.route('/productos', methods=['POST'])
def crear_producto():
    datos = request.get_json()
    if not datos or 'nombre' not in datos or 'precio' not in datos:
        abort(400)
    nuevo = {'id': len(productos) + 1, **datos}
    productos.append(nuevo)
    return jsonify(nuevo), 201

Escribir tests con el cliente de prueba

El cliente de prueba (test_client()) permite simular peticiones HTTP sin necesidad de ejecutar un servidor real:

# tests/test_rutas.py
import json

def test_listar_productos(cliente):
    """Verifica que se devuelven los productos correctamente."""
    respuesta = cliente.get('/productos')

    assert respuesta.status_code == 200
    datos = respuesta.get_json()
    assert isinstance(datos, list)
    assert len(datos) == 2

def test_obtener_producto_existente(cliente):
    """Verifica que se puede obtener un producto por su ID."""
    respuesta = cliente.get('/productos/1')

    assert respuesta.status_code == 200
    datos = respuesta.get_json()
    assert datos['id'] == 1
    assert datos['nombre'] == 'Laptop'

def test_obtener_producto_no_existente(cliente):
    """Verifica que se devuelve 404 para productos inexistentes."""
    respuesta = cliente.get('/productos/9999')

    assert respuesta.status_code == 404

def test_crear_producto(cliente):
    """Verifica la creación de un nuevo producto."""
    nuevo_producto = {
        'nombre': 'Teclado',
        'precio': 75.00
    }

    respuesta = cliente.post(
        '/productos',
        data=json.dumps(nuevo_producto),
        content_type='application/json'
    )

    assert respuesta.status_code == 201
    datos = respuesta.get_json()
    assert datos['nombre'] == 'Teclado'
    assert datos['precio'] == 75.00

def test_crear_producto_datos_incompletos(cliente):
    """Verifica que se rechaza la creación sin datos requeridos."""
    respuesta = cliente.post(
        '/productos',
        data=json.dumps({'nombre': 'Incompleto'}),
        content_type='application/json'
    )

    assert respuesta.status_code == 400

Estructura de tests en Flask con pytest

Verificar cabeceras y contenido de la respuesta

El cliente de prueba expone el objeto Response completo de Werkzeug, que permite inspeccionar todos los aspectos de la respuesta:

def test_cabeceras_json(cliente):
    """Verifica que la respuesta incluye cabeceras JSON correctas."""
    respuesta = cliente.get('/productos')

    # Verificar tipo de contenido
    assert respuesta.content_type == 'application/json'

    # Verificar cabeceras específicas
    assert 'application/json' in respuesta.headers['Content-Type']

def test_respuesta_vacia_post(cliente):
    """Verifica el comportamiento con cuerpo vacío en POST."""
    respuesta = cliente.post(
        '/productos',
        content_type='application/json'
    )

    assert respuesta.status_code == 400

def test_metodos_no_permitidos(cliente):
    """Verifica que métodos HTTP no permitidos devuelven 405."""
    respuesta = cliente.delete('/productos')
    assert respuesta.status_code == 405

Tests con contexto de aplicación

Algunos tests requieren acceso directo al contexto de aplicación para interactuar con la base de datos o extensiones:

# tests/test_base_de_datos.py
from app.models import Usuario
from app.extensions import db

def test_crear_usuario_en_db(app, db):
    """Verifica la creación de usuarios en la base de datos."""
    with app.app_context():
        usuario = Usuario(nombre='Ana', email='ana@ejemplo.com')
        db.session.add(usuario)
        db.session.commit()

        usuario_guardado = db.session.execute(
            db.select(Usuario).filter_by(email='ana@ejemplo.com')
        ).scalar_one_or_none()
        assert usuario_guardado is not None
        assert usuario_guardado.nombre == 'Ana'

        # Limpieza
        db.session.delete(usuario_guardado)
        db.session.commit()

def test_unicidad_email(app, db):
    """Verifica que no se pueden crear dos usuarios con el mismo email."""
    import pytest
    from sqlalchemy.exc import IntegrityError

    with app.app_context():
        usuario1 = Usuario(nombre='Carlos', email='carlos@ejemplo.com')
        db.session.add(usuario1)
        db.session.commit()

        with pytest.raises(IntegrityError):
            usuario2 = Usuario(nombre='Carlos 2', email='carlos@ejemplo.com')
            db.session.add(usuario2)
            db.session.commit()

        db.session.rollback()
        db.session.delete(usuario1)
        db.session.commit()

Parametrización de tests

pytest.mark.parametrize permite ejecutar el mismo test con múltiples conjuntos de datos, evitando duplicación de código:

import pytest

@pytest.mark.parametrize('url,codigo_esperado', [
    ('/productos', 200),
    ('/productos/1', 200),
    ('/productos/9999', 404),
    ('/ruta-inexistente', 404),
])
def test_codigos_de_estado(cliente, url, codigo_esperado):
    """Verifica los códigos de estado para diferentes rutas."""
    respuesta = cliente.get(url)
    assert respuesta.status_code == codigo_esperado

@pytest.mark.parametrize('datos,codigo_esperado', [
    ({'nombre': 'Producto válido', 'precio': 10.0}, 201),
    ({'nombre': 'Sin precio'}, 400),
    ({}, 400),
    (None, 400),
])
def test_validacion_creacion_producto(cliente, datos, codigo_esperado):
    """Verifica la validación de datos al crear productos."""
    import json
    respuesta = cliente.post(
        '/productos',
        data=json.dumps(datos) if datos else b'',
        content_type='application/json'
    )
    assert respuesta.status_code == codigo_esperado

Ejecutar los tests

Para ejecutar todos los tests del proyecto:

# Ejecutar todos los tests
pytest

# Ejecutar con salida detallada
pytest -v

# Ejecutar un archivo específico
pytest tests/test_rutas.py

# Ejecutar con cobertura de código
pip install pytest-cov
pytest --cov=app --cov-report=html

# Ejecutar solo tests con una marca específica
pytest -m "not slow"

La cobertura de código ayuda a identificar qué partes del código no están siendo probadas. Un objetivo razonable para proyectos Flask es 80-90% de cobertura.

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

Instalar y configurar pytest para proyectos Flask. Crear fixtures de aplicación y cliente de prueba con conftest.py. Escribir tests unitarios e de integración para rutas y modelos. Usar el cliente de prueba para simular peticiones HTTP. Organizar la suite de pruebas con carpetas y convenciones de nomenclatura.