Pruebas de Flask

Las pruebas son la base para garantizar la calidad del código.

Flask proporciona un cliente de pruebas integrado que, junto con pytest, permite escribir y ejecutar pruebas de manera eficiente.


Cliente de pruebas — test_client

El método de Flasktest_client()crea un cliente HTTP simulado que puede enviar solicitudes sin iniciar el servidor:

Ejemplo

# Ruta del archivo: test_demo.py (probar en el entorno interactivo de Python)
from app import create_app

# Crear una instancia de app para pruebas
app = create_app()

# Crear el cliente de pruebas
with app.test_client() as client:
    # Enviar una solicitud GET
    response = client.get("/")
    print(response.status_code)   # 200
    print(response.data[:100])    # Los primeros 100 bytes del cuerpo de la respuesta

    # Enviar una solicitud POST y enviar datos en JSON
    response = client.post("/api/posts",
                          json={"title": Tutorial EJEMPLO, "body": Contenido, "author_id": 1})
    print(response.status_code)   # 201
    print(response.get_json())    # Analizar la respuesta JSON

Integración con pytest

pytest es el framework de pruebas estándar de la comunidad de Python y es muy conveniente usarlo junto con Flask.

Instalación de pytest

(.venv) $ pip install pytest

Escribir pruebas — conftest.py (fixtures compartidos)

Usaconftest.pyel archivo para crear fixtures de prueba reutilizables:

Ejemplo

# Ruta del archivo: tests/conftest.py
import os
import tempfile
import pytest
from app import create_app
from db import init_db, get_db

@pytest.fixture
def app():
    """Crea una instancia de app para pruebas, usando una base de datos temporal"""
    # Crear un archivo temporal como base de datos de prueba
    db_fd, db_path = tempfile.mkstemp()

    # Crear la app y sobrescribir la ruta de la base de datos al archivo temporal
    app = create_app()
    app.config["DATABASE"] = db_path
    app.config["TESTING"] = True  # Habilitar el modo de prueba

    # Inicializar la estructura de tablas de la base de datos de prueba
    with app.app_context():
        init_db()

    yield app

    # Limpieza posterior a la prueba: cerrar y eliminar el archivo de base de datos temporal
    os.close(db_fd)
    os.unlink(db_path)

@pytest.fixture
def client(app):
    """Crea el cliente de pruebas"""
    return app.test_client()

@pytest.fixture
def runner(app):
    """Crea el ejecutor de pruebas CLI"""
    return app.test_cli_runner()

Probar rutas

Ejemplo

# Ruta del archivo: tests/test_app.py
def test_home_page(client):
    """Comprueba que la página de inicio responde correctamente"""
    response = client.get("/")
    assert response.status_code == 200
    # Verificar si la página contiene la palabra clave esperada
    assert b"EJEMPLO" in response.data

def test_404_page(client):
    """Prueba el acceso a una página inexistente"""
    response = client.get("/this-page-does-not-exist")
    assert response.status_code == 404

Ejecutar las pruebas:

(.venv) $ pytest tests/test_app.py -v
====================================
tests/test_app.py::test_home_page PASSED
tests/test_app.py::test_404_page PASSED
====================================

Probar la API JSON

Ejemplo

# Ruta del archivo: tests/test_api.py
def test_create_post(client):
    """Prueba la API de creación de artículos"""
    response = client.post("/api/posts", json={
        "title": Artículo de prueba EJEMPLO,
        "body": Este es el contenido de prueba,
        "author_id": 1
    })
    # La creación exitosa debe devolver 201
    assert response.status_code == 201
    data = response.get_json()
    assert data["title"] == Artículo de prueba EJEMPLO
    assert "id" in data

def test_create_post_without_title(client):
    """Prueba que se devuelva un error 400 cuando el título está vacío"""
    response = client.post("/api/posts", json={
        "title": "",
        "body": Contenido,
        "author_id": 1
    })
    assert response.status_code == 400
    assert b"error" in response.data

def test_get_posts(client):
    """Prueba la obtención de la lista de artículos"""
    # Crear primero un artículo
    client.post("/api/posts", json={
        "title": Artículo A, "body": Contenido A, "author_id": 1
    })

    # Obtener la lista
    response = client.get("/api/posts")
    assert response.status_code == 200
    data = response.get_json()
    assert len(data) >= 1

def test_delete_post(client):
    """Prueba la eliminación de un artículo"""
    # Crear primero
    rv = client.post("/api/posts", json={
        "title": Pendiente de eliminar, "body": Contenido, "author_id": 1
    })
    post_id = rv.get_json()["id"]

    # Luego eliminar
    rv = client.delete(f"/api/posts/{post_id}")
    assert rv.status_code == 200

    # Confirmar que se ha eliminado
    rv = client.get(f"/api/posts/{post_id}")
    assert rv.status_code == 404

Probar Session

Prueba funciones que requieren Session, como el inicio de sesión:

Ejemplo

# Ruta del archivo: tests/test_auth.py
def test_login(client):
    """Prueba el flujo de inicio de sesión"""
    # Enviar la solicitud de inicio de sesión
    response = client.post("/auth/login", data={
        "username": "testuser",
        "password": "testpass"
    }, follow_redirects=True)  # follow_redirects sigue automáticamente las redirecciones

    assert response.status_code == 200

def test_session_transaction(client):
    """Operar directamente sobre la sesión para hacer pruebas"""
    with client.session_transaction() as session:
        # Establecer valores directamente en la sesión para simular el estado de usuario autenticado
        session["username"] = "ejemplo"

    # Ahora, al acceder a la página de inicio, debería mostrarse el estado de usuario autenticado
    response = client.get("/")
    assert b"ejemplo" in response.data

client.session_transaction()Es una herramienta de prueba muy práctica: te permite modificar directamente el contenido de la Session sin necesidad de pasar por el flujo completo de inicio de sesión.


Probar comandos CLI

Ejemplo

# Ruta del archivo: tests/test_cli.py
def test_init_db(runner):
    """Prueba el comando de inicialización de la base de datos"""
    result = runner.invoke(args=["init-db"])
    # Si el comando se ejecuta correctamente, exit_code debe ser 0
    assert result.exit_code == 0

def test_flask_routes(runner):
    """Prueba el comando flask routes"""
    result = runner.invoke(args=["routes"])
    assert result.exit_code == 0
    # Verificar si la salida contiene las rutas registradas
    assert "/api/posts" in result.output

Buenas prácticas de pruebas

Práctica Descripción
Usar una base de datos temporal Cada prueba usa una base de datos independiente, sin interferencias entre sí (por ejemplo, tempfile en conftest.py)
Activar el modo TESTING app.config["TESTING"] = TruePermite que las excepciones se propaguen normalmente en lugar de ser absorbidas por el manejo de errores
Una prueba verifica solo una cosa Cada función test valida solo un comportamiento, lo que facilita localizar el problema
Nombres claros El nombre de la función debe tener la formatest_qué_escenario_qué_resultado_esperado
Aprovechar follow_redirects Muy útil al probar flujos con redirección después de un POST

Ejecutar todas las pruebas

(.venv) $ pytest tests/ -v

tests/test_app.py::test_home_page PASSED
tests/test_app.py::test_404_page PASSED
tests/test_api.py::test_create_post PASSED
tests/test_api.py::test_create_post_without_title PASSED
tests/test_api.py::test_get_posts PASSED
tests/test_api.py::test_delete_post PASSED
tests/test_auth.py::test_login PASSED
tests/test_auth.py::test_session_transaction PASSED
tests/test_cli.py::test_init_db PASSED
tests/test_cli.py::test_flask_routes PASSED

==================== 10 passed in 0.45s ====================
Otras extensiones