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
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
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
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
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
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
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