Saltar a contenido

Pruebas

🌐 Traducción por IA y humanos

Esta traducción fue hecha por IA guiada por humanos. 🤝

Podría tener errores al interpretar el significado original, o sonar poco natural, etc. 🤖

Puedes mejorar esta traducción ayudándonos a guiar mejor al LLM de IA.

Versión en inglés

Gracias a Starlette, escribir pruebas para aplicaciones de FastAPI es fácil y agradable.

Está basado en HTTPX, que a su vez está diseñado basado en Requests, por lo que es muy familiar e intuitivo.

Con él, puedes usar pytest directamente con FastAPI.

Usando TestClient

Nota

Para usar TestClient, primero instala httpx.

Asegúrate de crear un entorno virtual, activarlo y luego instalarlo, por ejemplo:

$ pipinstallhttpx

Importa TestClient.

Crea un TestClient pasándole tu aplicación de FastAPI.

Crea funciones con un nombre que comience con test_ (esta es la convención estándar de pytest).

Usa el objeto TestClient de la misma manera que con httpx.

Escribe statements assert simples con las expresiones estándar de Python que necesites revisar (otra vez, estándar de pytest).

fromfastapiimport FastAPI
fromfastapi.testclientimport TestClient
app = FastAPI()
@app.get("/")
async defread_main():
 return {"msg": "Hello World"}
client = TestClient(app)
deftest_read_main():
 response = client.get("/")
 assert response.status_code == 200
 assert response.json() == {"msg": "Hello World"}

Consejo

Nota que las funciones de prueba son def normales, no async def.

Y las llamadas al cliente también son llamadas normales, sin usar await.

Esto te permite usar pytest directamente sin complicaciones.

Detalles técnicos

También podrías usar from starlette.testclient import TestClient.

FastAPI proporciona el mismo starlette.testclient como fastapi.testclient solo por conveniencia para ti, el desarrollador. Pero proviene directamente de Starlette.

Consejo

Si quieres llamar a funciones async en tus pruebas además de enviar requests a tu aplicación FastAPI (por ejemplo, funciones asincrónicas de bases de datos), echa un vistazo a las Pruebas Asincrónicas en el tutorial avanzado.

Separando pruebas

En una aplicación real, probablemente tendrías tus pruebas en un archivo diferente.

Y tu aplicación de FastAPI también podría estar compuesta de varios archivos/módulos, etc.

Archivo de aplicación FastAPI

Digamos que tienes una estructura de archivos como se describe en Aplicaciones Más Grandes:

.
├── app
│  ├── __init__.py
│  └── main.py

En el archivo main.py tienes tu aplicación de FastAPI:

fromfastapiimport FastAPI
app = FastAPI()
@app.get("/")
async defread_main():
 return {"msg": "Hello World"}

Archivo de prueba

Entonces podrías tener un archivo test_main.py con tus pruebas. Podría estar en el mismo paquete de Python (el mismo directorio con un archivo __init__.py):

.
├── app
│  ├── __init__.py
│  ├── main.py
│  └── test_main.py

Debido a que este archivo está en el mismo paquete, puedes usar imports relativos para importar el objeto app desde el módulo main (main.py):

fromfastapi.testclientimport TestClient
from.mainimport app
client = TestClient(app)
deftest_read_main():
 response = client.get("/")
 assert response.status_code == 200
 assert response.json() == {"msg": "Hello World"}

...y tener el código para las pruebas tal como antes.

Pruebas: ejemplo extendido

Ahora extiende este ejemplo y añade más detalles para ver cómo escribir pruebas para diferentes partes.

Archivo de aplicación FastAPI extendido

Continuemos con la misma estructura de archivos que antes:

.
├── app
│  ├── __init__.py
│  ├── main.py
│  └── test_main.py

Digamos que ahora el archivo main.py con tu aplicación de FastAPI tiene algunas otras path operations.

Tiene una operación GET que podría devolver un error.

Tiene una operación POST que podría devolver varios errores.

Ambas path operations requieren un X-Token header.

fromtypingimport Annotated
fromfastapiimport FastAPI, Header, HTTPException
frompydanticimport BaseModel
fake_secret_token = "coneofsilence"
fake_db = {
 "foo": {"id": "foo", "title": "Foo", "description": "There goes my hero"},
 "bar": {"id": "bar", "title": "Bar", "description": "The bartenders"},
}
app = FastAPI()
classItem(BaseModel):
 id: str
 title: str
 description: str | None = None
@app.get("/items/{item_id}", response_model=Item)
async defread_main(item_id: str, x_token: Annotated[str, Header()]):
 if x_token != fake_secret_token:
 raise HTTPException(status_code=400, detail="Invalid X-Token header")
 if item_id not in fake_db:
 raise HTTPException(status_code=404, detail="Item not found")
 return fake_db[item_id]
@app.post("/items/")
async defcreate_item(item: Item, x_token: Annotated[str, Header()]) -> Item:
 if x_token != fake_secret_token:
 raise HTTPException(status_code=400, detail="Invalid X-Token header")
 if item.id in fake_db:
 raise HTTPException(status_code=409, detail="Item already exists")
 fake_db[item.id] = item.model_dump()
 return item
🤓 Other versions and variants

Tip

Prefer to use the Annotated version if possible.

fromfastapiimport FastAPI, Header, HTTPException
frompydanticimport BaseModel
fake_secret_token = "coneofsilence"
fake_db = {
 "foo": {"id": "foo", "title": "Foo", "description": "There goes my hero"},
 "bar": {"id": "bar", "title": "Bar", "description": "The bartenders"},
}
app = FastAPI()
classItem(BaseModel):
 id: str
 title: str
 description: str | None = None
@app.get("/items/{item_id}", response_model=Item)
async defread_main(item_id: str, x_token: str = Header()):
 if x_token != fake_secret_token:
 raise HTTPException(status_code=400, detail="Invalid X-Token header")
 if item_id not in fake_db:
 raise HTTPException(status_code=404, detail="Item not found")
 return fake_db[item_id]
@app.post("/items/")
async defcreate_item(item: Item, x_token: str = Header()) -> Item:
 if x_token != fake_secret_token:
 raise HTTPException(status_code=400, detail="Invalid X-Token header")
 if item.id in fake_db:
 raise HTTPException(status_code=409, detail="Item already exists")
 fake_db[item.id] = item.model_dump()
 return item

Archivo de prueba extendido

Podrías entonces actualizar test_main.py con las pruebas extendidas:

fromfastapi.testclientimport TestClient
from.mainimport app
client = TestClient(app)
deftest_read_item():
 response = client.get("/items/foo", headers={"X-Token": "coneofsilence"})
 assert response.status_code == 200
 assert response.json() == {
 "id": "foo",
 "title": "Foo",
 "description": "There goes my hero",
 }
deftest_read_item_bad_token():
 response = client.get("/items/foo", headers={"X-Token": "hailhydra"})
 assert response.status_code == 400
 assert response.json() == {"detail": "Invalid X-Token header"}
deftest_read_nonexistent_item():
 response = client.get("/items/baz", headers={"X-Token": "coneofsilence"})
 assert response.status_code == 404
 assert response.json() == {"detail": "Item not found"}
deftest_create_item():
 response = client.post(
 "/items/",
 headers={"X-Token": "coneofsilence"},
 json={"id": "foobar", "title": "Foo Bar", "description": "The Foo Barters"},
 )
 assert response.status_code == 200
 assert response.json() == {
 "id": "foobar",
 "title": "Foo Bar",
 "description": "The Foo Barters",
 }
deftest_create_item_bad_token():
 response = client.post(
 "/items/",
 headers={"X-Token": "hailhydra"},
 json={"id": "bazz", "title": "Bazz", "description": "Drop the bazz"},
 )
 assert response.status_code == 400
 assert response.json() == {"detail": "Invalid X-Token header"}
deftest_create_existing_item():
 response = client.post(
 "/items/",
 headers={"X-Token": "coneofsilence"},
 json={
 "id": "foo",
 "title": "The Foo ID Stealers",
 "description": "There goes my stealer",
 },
 )
 assert response.status_code == 409
 assert response.json() == {"detail": "Item already exists"}
🤓 Other versions and variants

Tip

Prefer to use the Annotated version if possible.

fromfastapi.testclientimport TestClient
from.mainimport app
client = TestClient(app)
deftest_read_item():
 response = client.get("/items/foo", headers={"X-Token": "coneofsilence"})
 assert response.status_code == 200
 assert response.json() == {
 "id": "foo",
 "title": "Foo",
 "description": "There goes my hero",
 }
deftest_read_item_bad_token():
 response = client.get("/items/foo", headers={"X-Token": "hailhydra"})
 assert response.status_code == 400
 assert response.json() == {"detail": "Invalid X-Token header"}
deftest_read_nonexistent_item():
 response = client.get("/items/baz", headers={"X-Token": "coneofsilence"})
 assert response.status_code == 404
 assert response.json() == {"detail": "Item not found"}
deftest_create_item():
 response = client.post(
 "/items/",
 headers={"X-Token": "coneofsilence"},
 json={"id": "foobar", "title": "Foo Bar", "description": "The Foo Barters"},
 )
 assert response.status_code == 200
 assert response.json() == {
 "id": "foobar",
 "title": "Foo Bar",
 "description": "The Foo Barters",
 }
deftest_create_item_bad_token():
 response = client.post(
 "/items/",
 headers={"X-Token": "hailhydra"},
 json={"id": "bazz", "title": "Bazz", "description": "Drop the bazz"},
 )
 assert response.status_code == 400
 assert response.json() == {"detail": "Invalid X-Token header"}
deftest_create_existing_item():
 response = client.post(
 "/items/",
 headers={"X-Token": "coneofsilence"},
 json={
 "id": "foo",
 "title": "The Foo ID Stealers",
 "description": "There goes my stealer",
 },
 )
 assert response.status_code == 409
 assert response.json() == {"detail": "Item already exists"}

Cada vez que necesites que el cliente pase información en el request y no sepas cómo, puedes buscar (Googlear) cómo hacerlo en httpx, o incluso cómo hacerlo con requests, dado que el diseño de HTTPX está basado en el diseño de Requests.

Luego simplemente haces lo mismo en tus pruebas.

Por ejemplo:

  • Para pasar un parámetro de path o query, añádelo a la URL misma.
  • Para pasar un cuerpo JSON, pasa un objeto de Python (por ejemplo, un dict) al parámetro json.
  • Si necesitas enviar Form Data en lugar de JSON, usa el parámetro data en su lugar.
  • Para pasar headers, usa un dict en el parámetro headers.
  • Para cookies, un dict en el parámetro cookies.

Para más información sobre cómo pasar datos al backend (usando httpx o el TestClient) revisa la documentación de HTTPX.

Nota

Ten en cuenta que el TestClient recibe datos que pueden ser convertidos a JSON, no modelos de Pydantic.

Si tienes un modelo de Pydantic en tu prueba y quieres enviar sus datos a la aplicación durante las pruebas, puedes usar el jsonable_encoder descrito en Codificador Compatible con JSON.

Ejecútalo

Después de eso, solo necesitas instalar pytest.

Asegúrate de crear un entorno virtual, activarlo y luego instalarlo, por ejemplo:

$ pipinstallpytest
---> 100%

Detectará los archivos y pruebas automáticamente, ejecutará las mismas y te reportará los resultados.

Ejecuta las pruebas con:

$ pytest
================ test session starts ================
platform linux -- Python 3.6.9, pytest-5.3.5, py-1.8.1, pluggy-0.13.1
rootdir: /home/user/code/superawesome-cli/app
plugins: forked-1.1.3, xdist-1.31.0, cov-2.8.1
collected 6 items
---> 100%
test_main.py <span style="color: green; white-space: pre;">...... [100%]</span>
<span style="color: green;">================= 1 passed in 0.03s =================</span>

AltStyle によって変換されたページ (->オリジナル) /