Aprenda c贸mo probar eficazmente sus aplicaciones FastAPI usando TestClient. Cubre las mejores pr谩cticas, t茅cnicas avanzadas y ejemplos del mundo real para APIs robustas y confiables.
Dominando las Pruebas en FastAPI: Una Gu铆a Completa de TestClient
FastAPI ha surgido como un marco de trabajo l铆der para la construcci贸n de APIs de alto rendimiento con Python. Su velocidad, facilidad de uso y validaci贸n autom谩tica de datos lo convierten en un favorito entre los desarrolladores de todo el mundo. Sin embargo, una API bien construida es tan buena como sus pruebas. Las pruebas exhaustivas garantizan que su API funcione como se espera, se mantenga estable bajo presi贸n y pueda implementarse con confianza en producci贸n. Esta gu铆a completa se centra en el uso de TestClient de FastAPI para probar eficazmente los puntos finales de su API.
驴Por qu茅 son Importantes las Pruebas para las Aplicaciones FastAPI?
Las pruebas son un paso crucial en el ciclo de vida del desarrollo de software. Le ayudan a:
- Identificar errores temprano: Detectar errores antes de que lleguen a producci贸n, ahorrando tiempo y recursos.
- Garantizar la calidad del c贸digo: Promover un c贸digo bien estructurado y mantenible.
- Prevenir regresiones: Garantizar que los nuevos cambios no rompan la funcionalidad existente.
- Mejorar la fiabilidad de la API: Generar confianza en la estabilidad y el rendimiento de la API.
- Facilitar la colaboraci贸n: Proporcionar documentaci贸n clara del comportamiento esperado para otros desarrolladores.
Introducci贸n a TestClient de FastAPI
FastAPI proporciona un TestClient incorporado que simplifica el proceso de prueba de los puntos finales de su API. El TestClient act煤a como un cliente ligero que puede enviar solicitudes a su API sin iniciar un servidor completo. Esto hace que las pruebas sean significativamente m谩s r谩pidas y convenientes.
Caracter铆sticas Clave de TestClient:
- Simula solicitudes HTTP: Le permite enviar solicitudes GET, POST, PUT, DELETE y otras solicitudes HTTP a su API.
- Maneja la serializaci贸n de datos: Serializa autom谩ticamente los datos de la solicitud (por ejemplo, cargas 煤tiles JSON) y deserializa los datos de la respuesta.
- Proporciona m茅todos de aserci贸n: Ofrece m茅todos convenientes para verificar el c贸digo de estado, los encabezados y el contenido de las respuestas.
- Admite pruebas as铆ncronas: Funciona a la perfecci贸n con la naturaleza as铆ncrona de FastAPI.
- Se integra con marcos de trabajo de pruebas: Se integra f谩cilmente con marcos de trabajo de pruebas de Python populares como pytest y unittest.
Configuraci贸n de su Entorno de Pruebas
Antes de comenzar a probar, debe configurar su entorno de pruebas. Esto generalmente implica instalar las dependencias necesarias y configurar su marco de trabajo de pruebas.
Instalaci贸n
Primero, aseg煤rese de tener FastAPI y pytest instalados. Puede instalarlos usando pip:
pip install fastapi pytest httpx
httpx es un cliente HTTP que FastAPI utiliza internamente. Si bien TestClient es parte de FastAPI, tener httpx instalado tambi茅n garantiza pruebas sin problemas. Algunos tutoriales tambi茅n mencionan requests, sin embargo, httpx est谩 m谩s alineado con la naturaleza as铆ncrona de FastAPI.
Ejemplo de Aplicaci贸n FastAPI
Creemos una aplicaci贸n FastAPI simple que podamos usar para las pruebas:
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
description: str | None = None
price: float
tax: float | None = None
@app.get("/")
async def read_root():
return {"message": "Hello World"}
@app.get("/items/{item_id}")
async def read_item(item_id: int, q: str | None = None):
return {"item_id": item_id, "q": q}
@app.post("/items/")
async def create_item(item: Item):
return item
Guarde este c贸digo como main.py. Esta aplicaci贸n define tres puntos finales:
/: Un punto final GET simple que devuelve un mensaje "Hello World"./items/{item_id}: Un punto final GET que devuelve un elemento seg煤n su ID./items/: Un punto final POST que crea un nuevo elemento.
Escribiendo su Primera Prueba
Ahora que tiene una aplicaci贸n FastAPI, puede comenzar a escribir pruebas usando TestClient. Cree un nuevo archivo llamado test_main.py en el mismo directorio que main.py.
from fastapi.testclient import TestClient
from .main import app
client = TestClient(app)
def test_read_root():
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"message": "Hello World"}
En esta prueba:
- Importamos
TestClienty la instanciaappde FastAPI. - Creamos una instancia de
TestClient, pasandoapp. - Definimos una funci贸n de prueba
test_read_root. - Dentro de la funci贸n de prueba, usamos
client.get("/")para enviar una solicitud GET al punto final ra铆z. - Afirmamos que el c贸digo de estado de la respuesta es 200 (OK).
- Afirmamos que el JSON de la respuesta es igual a
{"message": "Hello World"}.
Ejecutando sus Pruebas con pytest
Para ejecutar sus pruebas, simplemente abra una terminal en el directorio que contiene su archivo test_main.py y ejecute el siguiente comando:
pytest
pytest descubrir谩 y ejecutar谩 autom谩ticamente todas las pruebas en su proyecto. Deber铆a ver una salida similar a esta:
============================= test session starts ==============================
platform darwin -- Python 3.9.6, pytest-6.2.5, py-1.11.0, pluggy-1.0.0
rootdir: /path/to/your/project
collected 1 item
test_main.py .
============================== 1 passed in 0.01s ===============================
Probando Diferentes M茅todos HTTP
TestClient admite todos los m茅todos HTTP est谩ndar, incluidos GET, POST, PUT, DELETE y PATCH. Veamos c贸mo probar cada uno de estos m茅todos.
Probando Solicitudes GET
Ya vimos un ejemplo de c贸mo probar una solicitud GET en la secci贸n anterior. Aqu铆 hay otro ejemplo, probando el punto final /items/{item_id}:
def test_read_item():
response = client.get("/items/1?q=test")
assert response.status_code == 200
assert response.json() == {"item_id": 1, "q": "test"}
Esta prueba env铆a una solicitud GET a /items/1 con un par谩metro de consulta q=test. Luego, afirma que el c贸digo de estado de la respuesta es 200 y que el JSON de la respuesta contiene los datos esperados.
Probando Solicitudes POST
Para probar una solicitud POST, debe enviar datos en el cuerpo de la solicitud. TestClient serializa autom谩ticamente los datos a JSON.
def test_create_item():
item_data = {"name": "Example Item", "description": "A test item", "price": 9.99, "tax": 1.00}
response = client.post("/items/", json=item_data)
assert response.status_code == 200
assert response.json() == item_data
En esta prueba:
- Creamos un diccionario
item_dataque contiene los datos para el nuevo elemento. - Usamos
client.post("/items/", json=item_data)para enviar una solicitud POST al punto final/items/, pasandoitem_datacomo la carga 煤til JSON. - Afirmamos que el c贸digo de estado de la respuesta es 200 y que el JSON de la respuesta coincide con
item_data.
Probando Solicitudes PUT, DELETE y PATCH
Probar las solicitudes PUT, DELETE y PATCH es similar a probar las solicitudes POST. Simplemente use los m茅todos correspondientes en TestClient:
def test_update_item():
item_data = {"name": "Updated Item", "description": "An updated test item", "price": 19.99, "tax": 2.00}
response = client.put("/items/1", json=item_data)
assert response.status_code == 200
# Add assertions for the expected response
def test_delete_item():
response = client.delete("/items/1")
assert response.status_code == 200
# Add assertions for the expected response
def test_patch_item():
item_data = {"price": 29.99}
response = client.patch("/items/1", json=item_data)
assert response.status_code == 200
# Add assertions for the expected response
Recuerde agregar aserciones para verificar que las respuestas sean las esperadas.
T茅cnicas de Pruebas Avanzadas
TestClient ofrece varias caracter铆sticas avanzadas que pueden ayudarle a escribir pruebas m谩s completas y eficaces.
Pruebas con Dependencias
El sistema de inyecci贸n de dependencias de FastAPI le permite inyectar f谩cilmente dependencias en los puntos finales de su API. Al realizar pruebas, es posible que desee anular estas dependencias para proporcionar implementaciones simuladas o espec铆ficas de la prueba.
Por ejemplo, suponga que su aplicaci贸n depende de una conexi贸n de base de datos. Puede anular la dependencia de la base de datos en sus pruebas para usar una base de datos en memoria:
from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.orm import sessionmaker, declarative_base, Session
# Database Configuration
DATABASE_URL = "sqlite:///./test.db" # In-memory database for testing
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
# Define User Model
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
username = Column(String, unique=True, index=True)
password = Column(String)
Base.metadata.create_all(bind=engine)
# FastAPI App
app = FastAPI()
# Dependency to get the database session
def get_db():
db = TestingSessionLocal()
try:
yield db
finally:
db.close()
# Endpoint to create a user
@app.post("/users/")
async def create_user(username: str, password: str, db: Session = Depends(get_db)):
db_user = User(username=username, password=password)
db.add(db_user)
db.commit()
db.refresh(db_user)
return db_user
from fastapi.testclient import TestClient
from .main import app, get_db, Base, engine, TestingSessionLocal
client = TestClient(app)
# Override the database dependency for testing
def override_get_db():
try:
db = TestingSessionLocal()
yield db
finally:
db.close()
app.dependency_overrides[get_db] = override_get_db
def test_create_user():
# First, ensure the tables are created, which may not happen by default
Base.metadata.create_all(bind=engine) # important: create the tables in the test db
response = client.post("/users/", params={"username": "testuser", "password": "password123"})
assert response.status_code == 200
assert response.json()["username"] == "testuser"
# Clean up the override after the test if needed
app.dependency_overrides = {}
Este ejemplo anula la dependencia get_db con una funci贸n espec铆fica de la prueba que devuelve una sesi贸n a una base de datos SQLite en memoria. Importante: La creaci贸n de metadatos debe invocarse expl铆citamente para que la base de datos de prueba funcione correctamente. Si no se crea la tabla, se producir谩n errores relacionados con las tablas que faltan.
Pruebas de C贸digo As铆ncrono
FastAPI est谩 construido para ser as铆ncrono, por lo que a menudo necesitar谩 probar c贸digo as铆ncrono. TestClient admite las pruebas as铆ncronas sin problemas.
Para probar un punto final as铆ncrono, simplemente defina su funci贸n de prueba como async:
import asyncio
from fastapi import FastAPI
app = FastAPI()
@app.get("/async")
async def async_endpoint():
await asyncio.sleep(0.1) # Simulate some async operation
return {"message": "Async Hello"}
import pytest
from fastapi.testclient import TestClient
from .main import app
client = TestClient(app)
@pytest.mark.asyncio # Needed to be compatible with pytest-asyncio
async def test_async_endpoint():
response = client.get("/async")
assert response.status_code == 200
assert response.json() == {"message": "Async Hello"}
Nota: Necesita instalar pytest-asyncio para usar @pytest.mark.asyncio: pip install pytest-asyncio. Tambi茅n debe asegurarse de que asyncio.get_event_loop() est茅 configurado si usa versiones anteriores de pytest. Si usa pytest versi贸n 8 o m谩s reciente, es posible que esto no sea necesario.
Pruebas de Carga de Archivos
FastAPI facilita el manejo de la carga de archivos. Para probar la carga de archivos, puede usar el par谩metro files de los m茅todos de solicitud de TestClient.
from fastapi import FastAPI, File, UploadFile
from typing import List
app = FastAPI()
@app.post("/files/")
async def create_files(files: List[bytes] = File()):
return {"file_sizes": [len(file) for file in files]}
@app.post("/uploadfiles/")
async def create_upload_files(files: List[UploadFile]):
return {"filenames": [file.filename for file in files]}
from fastapi.testclient import TestClient
from .main import app
import io
client = TestClient(app)
def test_create_files():
file_content = b"Test file content"
files = [('files', ('test.txt', io.BytesIO(file_content), 'text/plain'))]
response = client.post("/files/", files=files)
assert response.status_code == 200
assert response.json() == {"file_sizes": [len(file_content)]}
def test_create_upload_files():
file_content = b"Test upload file content"
files = [('files', ('test_upload.txt', io.BytesIO(file_content), 'text/plain'))]
response = client.post("/uploadfiles/", files=files)
assert response.status_code == 200
assert response.json() == {"filenames": ["test_upload.txt"]}
En esta prueba, creamos un archivo ficticio usando io.BytesIO y lo pasamos al par谩metro files. El par谩metro files acepta una lista de tuplas, donde cada tupla contiene el nombre del campo, el nombre del archivo y el contenido del archivo. El tipo de contenido es importante para el manejo preciso por parte del servidor.
Pruebas de Manejo de Errores
Es importante probar c贸mo su API maneja los errores. Puede usar TestClient para enviar solicitudes no v谩lidas y verificar que la API devuelva las respuestas de error correctas.
from fastapi import FastAPI, HTTPException
app = FastAPI()
@app.get("/items/{item_id}")
async def read_item(item_id: int):
if item_id > 100:
raise HTTPException(status_code=400, detail="Item ID too large")
return {"item_id": item_id}
from fastapi.testclient import TestClient
from .main import app
client = TestClient(app)
def test_read_item_error():
response = client.get("/items/101")
assert response.status_code == 400
assert response.json() == {"detail": "Item ID too large"}
Esta prueba env铆a una solicitud GET a /items/101, que genera una HTTPException con un c贸digo de estado de 400. La prueba afirma que el c贸digo de estado de la respuesta es 400 y que el JSON de la respuesta contiene el mensaje de error esperado.
Pruebas de Caracter铆sticas de Seguridad
Si su API usa autenticaci贸n o autorizaci贸n, tambi茅n deber谩 probar estas caracter铆sticas de seguridad. TestClient le permite establecer encabezados y cookies para simular solicitudes autenticadas.
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
app = FastAPI()
# Security
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
@app.post("/token")
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
# Simulate authentication
if form_data.username != "testuser" or form_data.password != "password123":
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect username or password")
return {"access_token": "fake_token", "token_type": "bearer"}
@app.get("/protected")
async def protected_route(token: str = Depends(oauth2_scheme)):
return {"message": "Protected data"}
from fastapi.testclient import TestClient
from .main import app
client = TestClient(app)
def test_login():
response = client.post("/token", data={"username": "testuser", "password": "password123"})
assert response.status_code == 200
assert "access_token" in response.json()
def test_protected_route():
# First, get a token
token_response = client.post("/token", data={"username": "testuser", "password": "password123"})
token = token_response.json()["access_token"]
# Then, use the token to access the protected route
response = client.get("/protected", headers={"Authorization": f"Bearer {token}"}) # corrected format.
assert response.status_code == 200
assert response.json() == {"message": "Protected data"}
En este ejemplo, probamos el punto final de inicio de sesi贸n y luego usamos el 锌芯谢褍褔械薪薪褘泄 token para acceder a una ruta protegida. El par谩metro headers de los m茅todos de solicitud de TestClient le permite establecer encabezados personalizados, incluido el encabezado Authorization para los tokens de portador.
Mejores Pr谩cticas para las Pruebas de FastAPI
Estas son algunas de las mejores pr谩cticas a seguir al probar sus aplicaciones FastAPI:
- Escriba pruebas completas: Apunte a una alta cobertura de pruebas para asegurarse de que todas las partes de su API se prueben a fondo.
- Use nombres de prueba descriptivos: Aseg煤rese de que los nombres de sus pruebas indiquen claramente lo que est谩 verificando la prueba.
- Siga el patr贸n Organizar-Actuar-Afirmar: Organice sus pruebas en tres fases distintas: Organizar (configurar los datos de la prueba), Actuar (realizar la acci贸n que se est谩 probando) y Afirmar (verificar los resultados).
- Use objetos simulados: Simule las dependencias externas para aislar sus pruebas y evitar depender de sistemas externos.
- Pruebe los casos extremos: Pruebe su API con entradas no v谩lidas o inesperadas para asegurarse de que maneje los errores con elegancia.
- Ejecute las pruebas con frecuencia: Integre las pruebas en su flujo de trabajo de desarrollo para detectar errores de forma temprana y frecuente.
- Int茅grese con CI/CD: Automatice sus pruebas en su canalizaci贸n de CI/CD para asegurarse de que todos los cambios de c贸digo se prueben a fondo antes de implementarse en producci贸n. Se pueden usar herramientas como Jenkins, GitLab CI, GitHub Actions o CircleCI para lograr esto.
Ejemplo: Pruebas de Internacionalizaci贸n (i18n)
Al desarrollar APIs para una audiencia global, la internacionalizaci贸n (i18n) es esencial. Probar i18n implica verificar que su API sea compatible con varios idiomas y regiones correctamente. Aqu铆 hay un ejemplo de c贸mo puede probar i18n en una aplicaci贸n FastAPI:
from fastapi import FastAPI, Header
from typing import Optional
app = FastAPI()
messages = {
"en": {"greeting": "Hello, world!"},
"fr": {"greeting": "Bonjour le monde !"},
"es": {"greeting": "隆Hola Mundo!"},
}
@app.get("/")
async def read_root(accept_language: Optional[str] = Header(None)):
lang = accept_language[:2] if accept_language else "en"
if lang not in messages:
lang = "en"
return {"message": messages[lang]["greeting"]}
from fastapi.testclient import TestClient
from .main import app
client = TestClient(app)
def test_read_root_en():
response = client.get("/", headers={"Accept-Language": "en-US"})
assert response.status_code == 200
assert response.json() == {"message": "Hello, world!"}
def test_read_root_fr():
response = client.get("/", headers={"Accept-Language": "fr-FR"})
assert response.status_code == 200
assert response.json() == {"message": "Bonjour le monde !"}
def test_read_root_es():
response = client.get("/", headers={"Accept-Language": "es-ES"})
assert response.status_code == 200
assert response.json() == {"message": "隆Hola Mundo!"}
def test_read_root_default():
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"message": "Hello, world!"}
Este ejemplo establece el encabezado Accept-Language para especificar el idioma deseado. La API devuelve el saludo en el idioma especificado. Las pruebas garantizan que la API maneje correctamente las diferentes preferencias de idioma. Si el encabezado Accept-Language est谩 ausente, se usa el idioma predeterminado "en".
Conclusi贸n
Las pruebas son una parte esencial de la construcci贸n de aplicaciones FastAPI robustas y confiables. TestClient proporciona una forma sencilla y conveniente de probar los puntos finales de su API. Si sigue las mejores pr谩cticas descritas en esta gu铆a, puede escribir pruebas exhaustivas que garanticen la calidad y la estabilidad de sus APIs. Desde solicitudes b谩sicas hasta t茅cnicas avanzadas como la inyecci贸n de dependencias y las pruebas as铆ncronas, TestClient le permite crear c贸digo bien probado y mantenible. Adopte las pruebas como una parte fundamental de su flujo de trabajo de desarrollo y crear谩 APIs que sean poderosas y confiables para los usuarios de todo el mundo. Recuerde la importancia de la integraci贸n de CI/CD para automatizar las pruebas y garantizar el aseguramiento continuo de la calidad.