Testing¶
🌐 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ónayudándonos a guiar mejor al LLM de IA.
Gracias aStarlette, escribir pruebas para aplicaciones deFastAPI es fácil y agradable.
Está basado enHTTPX, que a su vez está diseñado basado en Requests, por lo que es muy familiar e intuitivo.
Con él, puedes usarpytest directamente conFastAPI.
UsandoTestClient¶
Información
Para usarTestClient, primero instalahttpx.
Asegúrate de crear unentorno virtual, activarlo y luego instalarlo, por ejemplo:
$pipinstallhttpxImportaTestClient.
Crea unTestClient pasándole tu aplicación deFastAPI.
Crea funciones con un nombre que comience contest_ (esta es la convención estándar depytest).
Usa el objetoTestClient de la misma manera que conhttpx.
Escribe declaracionesassert simples con las expresiones estándar de Python que necesites revisar (otra vez, estándar depytest).
fromfastapiimportFastAPIfromfastapi.testclientimportTestClientapp=FastAPI()@app.get("/")asyncdefread_main():return{"msg":"Hello World"}client=TestClient(app)deftest_read_main():response=client.get("/")assertresponse.status_code==200assertresponse.json()=={"msg":"Hello World"}Consejo
Nota que las funciones de prueba sondef normales, noasync def.
Y las llamadas al cliente también son llamadas normales, sin usarawait.
Esto te permite usarpytest directamente sin complicaciones.
Nota Técnica
También podrías usarfrom starlette.testclient import TestClient.
FastAPI proporciona el mismostarlette.testclient comofastapi.testclient solo por conveniencia para ti, el desarrollador. Pero proviene directamente de Starlette.
Consejo
Si quieres llamar a funcionesasync 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 lasPruebas 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 deFastAPI también podría estar compuesta de varios archivos/módulos, etc.
Archivo de aplicaciónFastAPI¶
Digamos que tienes una estructura de archivos como se describe enAplicaciones Más Grandes:
.├── app│ ├── __init__.py│ └── main.pyEn el archivomain.py tienes tu aplicación deFastAPI:
fromfastapiimportFastAPIapp=FastAPI()@app.get("/")asyncdefread_main():return{"msg":"Hello World"}Archivo de prueba¶
Entonces podrías tener un archivotest_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.pyDebido a que este archivo está en el mismo paquete, puedes usar importaciones relativas para importar el objetoapp desde el módulomain (main.py):
fromfastapi.testclientimportTestClientfrom.mainimportappclient=TestClient(app)deftest_read_main():response=client.get("/")assertresponse.status_code==200assertresponse.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ónFastAPI extendido¶
Continuemos con la misma estructura de archivos que antes:
.├── app│ ├── __init__.py│ ├── main.py│ └── test_main.pyDigamos que ahora el archivomain.py con tu aplicación deFastAPI tiene algunas otraspath operations.
Tiene una operaciónGET que podría devolver un error.
Tiene una operaciónPOST que podría devolver varios errores.
Ambaspath operations requieren unX-Token header.
fromtypingimportAnnotatedfromfastapiimportFastAPI,Header,HTTPExceptionfrompydanticimportBaseModelfake_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:strtitle:strdescription:str|None=None@app.get("/items/{item_id}",response_model=Item)asyncdefread_main(item_id:str,x_token:Annotated[str,Header()]):ifx_token!=fake_secret_token:raiseHTTPException(status_code=400,detail="Invalid X-Token header")ifitem_idnotinfake_db:raiseHTTPException(status_code=404,detail="Item not found")returnfake_db[item_id]@app.post("/items/")asyncdefcreate_item(item:Item,x_token:Annotated[str,Header()])->Item:ifx_token!=fake_secret_token:raiseHTTPException(status_code=400,detail="Invalid X-Token header")ifitem.idinfake_db:raiseHTTPException(status_code=409,detail="Item already exists")fake_db[item.id]=item.model_dump()returnitem🤓 Other versions and variants
Tip
Prefer to use theAnnotated version if possible.
fromfastapiimportFastAPI,Header,HTTPExceptionfrompydanticimportBaseModelfake_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:strtitle:strdescription:str|None=None@app.get("/items/{item_id}",response_model=Item)asyncdefread_main(item_id:str,x_token:str=Header()):ifx_token!=fake_secret_token:raiseHTTPException(status_code=400,detail="Invalid X-Token header")ifitem_idnotinfake_db:raiseHTTPException(status_code=404,detail="Item not found")returnfake_db[item_id]@app.post("/items/")asyncdefcreate_item(item:Item,x_token:str=Header())->Item:ifx_token!=fake_secret_token:raiseHTTPException(status_code=400,detail="Invalid X-Token header")ifitem.idinfake_db:raiseHTTPException(status_code=409,detail="Item already exists")fake_db[item.id]=item.model_dump()returnitemArchivo de prueba extendido¶
Podrías entonces actualizartest_main.py con las pruebas extendidas:
fromfastapi.testclientimportTestClientfrom.mainimportappclient=TestClient(app)deftest_read_item():response=client.get("/items/foo",headers={"X-Token":"coneofsilence"})assertresponse.status_code==200assertresponse.json()=={"id":"foo","title":"Foo","description":"There goes my hero",}deftest_read_item_bad_token():response=client.get("/items/foo",headers={"X-Token":"hailhydra"})assertresponse.status_code==400assertresponse.json()=={"detail":"Invalid X-Token header"}deftest_read_nonexistent_item():response=client.get("/items/baz",headers={"X-Token":"coneofsilence"})assertresponse.status_code==404assertresponse.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"},)assertresponse.status_code==200assertresponse.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"},)assertresponse.status_code==400assertresponse.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",},)assertresponse.status_code==409assertresponse.json()=={"detail":"Item already exists"}🤓 Other versions and variants
Tip
Prefer to use theAnnotated version if possible.
fromfastapi.testclientimportTestClientfrom.mainimportappclient=TestClient(app)deftest_read_item():response=client.get("/items/foo",headers={"X-Token":"coneofsilence"})assertresponse.status_code==200assertresponse.json()=={"id":"foo","title":"Foo","description":"There goes my hero",}deftest_read_item_bad_token():response=client.get("/items/foo",headers={"X-Token":"hailhydra"})assertresponse.status_code==400assertresponse.json()=={"detail":"Invalid X-Token header"}deftest_read_nonexistent_item():response=client.get("/items/baz",headers={"X-Token":"coneofsilence"})assertresponse.status_code==404assertresponse.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"},)assertresponse.status_code==200assertresponse.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"},)assertresponse.status_code==400assertresponse.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",},)assertresponse.status_code==409assertresponse.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 enhttpx, o incluso cómo hacerlo conrequests, 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 depath oquery, añádelo a la URL misma.
- Para pasar un cuerpo JSON, pasa un objeto de Python (por ejemplo, un
dict) al parámetrojson. - Si necesitas enviarForm Data en lugar de JSON, usa el parámetro
dataen su lugar. - Para pasarheaders, usa un
dicten el parámetroheaders. - Paracookies, un
dicten el parámetrocookies.
Para más información sobre cómo pasar datos al backend (usandohttpx o elTestClient) revisa ladocumentación de HTTPX.
Información
Ten en cuenta que elTestClient 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 eljsonable_encoder descrito enCodificador Compatible con JSON.
Ejecútalo¶
Después de eso, solo necesitas instalarpytest.
Asegúrate de crear unentorno 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.1rootdir: /home/user/code/superawesome-cli/appplugins: forked-1.1.3, xdist-1.31.0, cov-2.8.1collected 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>






