Tester¶
đ Traduction par IA et humains
Cette traduction a Ă©tĂ© rĂ©alisĂ©e par une IA guidĂ©e par des humains. đ€
Elle peut contenir des erreurs d'interprĂ©tation du sens original, ou paraĂźtre peu naturelle, etc. đ€
Vous pouvez améliorer cette traduction en nous aidant à mieux guider le LLM d'IA.
Grùce à Starlette, tester des applications FastAPI est simple et agréable.
Câest basĂ© sur HTTPX, dont la conception sâinspire de Requests, ce qui le rend trĂšs familier et intuitif.
Avec cela, vous pouvez utiliser pytest directement avec FastAPI.
Utiliser TestClient¶
Info
Pour utiliser TestClient, installez dâabord httpx.
Vous devez crĂ©er un environnement virtuel, lâactiver, puis y installer le paquet, par exemple :
$ pip install httpx
Importez TestClient.
Créez un TestClient en lui passant votre application FastAPI.
CrĂ©ez des fonctions dont le nom commence par test_ (câest la convention standard de pytest).
Utilisez lâobjet TestClient de la mĂȘme maniĂšre que vous utilisez httpx.
Ăcrivez de simples instructions assert avec les expressions Python standard que vous devez vĂ©rifier (lĂ encore, standard pytest).
from fastapi import FastAPI
from fastapi.testclient import TestClient
app = FastAPI()
@app.get("/")
async def read_main():
return {"msg": "Hello World"}
client = TestClient(app)
def test_read_main():
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"msg": "Hello World"}
Astuce
Remarquez que les fonctions de test sont des def normales, pas des async def.
Et les appels au client sont aussi des appels normaux, sans utiliser await.
Cela vous permet dâutiliser pytest directement sans complications.
Détails techniques
Vous pouvez aussi utiliser from starlette.testclient import TestClient.
FastAPI fournit le mĂȘme starlette.testclient sous le nom fastapi.testclient uniquement pour votre commoditĂ©, en tant que dĂ©veloppeur. Mais cela vient directement de Starlette.
Astuce
Si vous souhaitez appeler des fonctions async dans vos tests en dehors de lâenvoi de requĂȘtes Ă votre application FastAPI (par exemple des fonctions de base de donnĂ©es asynchrones), consultez les Tests asynchrones dans le tutoriel avancĂ©.
SĂ©parer les tests¶
Dans une application réelle, vous auriez probablement vos tests dans un fichier différent.
Et votre application FastAPI pourrait aussi ĂȘtre composĂ©e de plusieurs fichiers/modules, etc.
Fichier dâapplication FastAPI¶
Supposons que vous ayez une structure de fichiers comme décrit dans Applications plus grandes :
.
âââ app
â  âââ __init__.py
â  âââ main.py
Dans le fichier main.py, vous avez votre application FastAPIÂ :
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def read_main():
return {"msg": "Hello World"}
Fichier de test¶
Vous pourriez alors avoir un fichier test_main.py avec vos tests. Il pourrait vivre dans le mĂȘme package Python (le mĂȘme rĂ©pertoire avec un fichier __init__.py) :
.
âââ app
â  âââ __init__.py
â  âââ main.py
â  âââ test_main.py
Comme ce fichier se trouve dans le mĂȘme package, vous pouvez utiliser des imports relatifs pour importer lâobjet app depuis le module main (main.py)Â :
from fastapi.testclient import TestClient
from .main import app
client = TestClient(app)
def test_read_main():
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"msg": "Hello World"}
⊠et avoir le code des tests comme précédemment.
Tester : exemple Ă©tendu¶
Ătendons maintenant cet exemple et ajoutons plus de dĂ©tails pour voir comment tester diffĂ©rentes parties.
Fichier dâapplication FastAPI Ă©tendu¶
Continuons avec la mĂȘme structure de fichiers quâauparavant :
.
âââ app
â  âââ __init__.py
â  âââ main.py
â  âââ test_main.py
Supposons que dĂ©sormais le fichier main.py avec votre application FastAPI contienne dâautres chemins dâaccĂšs.
Il a une opération GET qui pourrait renvoyer une erreur.
Il a une opération POST qui pourrait renvoyer plusieurs erreurs.
Les deux chemins dâaccĂšs requiĂšrent un en-tĂȘte X-Token.
from typing import Annotated
from fastapi import FastAPI, Header, HTTPException
from pydantic import 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()
class Item(BaseModel):
id: str
title: str
description: str | None = None
@app.get("/items/{item_id}", response_model=Item)
async def read_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 def create_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.
from fastapi import FastAPI, Header, HTTPException
from pydantic import 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()
class Item(BaseModel):
id: str
title: str
description: str | None = None
@app.get("/items/{item_id}", response_model=Item)
async def read_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 def create_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
Fichier de test Ă©tendu¶
Vous pourriez ensuite mettre à jour test_main.py avec les tests étendus :
from fastapi.testclient import TestClient
from .main import app
client = TestClient(app)
def test_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",
}
def test_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"}
def test_read_nonexistent_item():
response = client.get("/items/baz", headers={"X-Token": "coneofsilence"})
assert response.status_code == 404
assert response.json() == {"detail": "Item not found"}
def test_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",
}
def test_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"}
def test_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.
from fastapi.testclient import TestClient
from .main import app
client = TestClient(app)
def test_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",
}
def test_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"}
def test_read_nonexistent_item():
response = client.get("/items/baz", headers={"X-Token": "coneofsilence"})
assert response.status_code == 404
assert response.json() == {"detail": "Item not found"}
def test_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",
}
def test_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"}
def test_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"}
Chaque fois que vous avez besoin que le client transmette des informations dans la requĂȘte et que vous ne savez pas comment faire, vous pouvez chercher (Google) comment le faire avec httpx, ou mĂȘme comment le faire avec requests, puisque la conception de HTTPX est basĂ©e sur celle de Requests.
Ensuite, vous faites simplement la mĂȘme chose dans vos tests.
Par exemple :
- Pour passer un paramĂštre de chemin ou un paramĂštre de requĂȘte, ajoutez-le directement Ă lâURL.
- Pour passer un corps JSON, passez un objet Python (par exemple un
dict) au paramĂštrejson. - Si vous devez envoyer des Form Data au lieu de JSON, utilisez le paramĂštre
dataĂ la place. - Pour passer des en-tĂȘtes, utilisez un
dictdans le paramĂštreheaders. - Pour les cookies, un
dictdans le paramĂštrecookies.
Pour plus dâinformations sur la maniĂšre de transmettre des donnĂ©es au backend (en utilisant httpx ou le TestClient), consultez la documentation HTTPX.
Info
Notez que le TestClient reçoit des donnĂ©es qui peuvent ĂȘtre converties en JSON, pas des modĂšles Pydantic.
Si vous avez un modĂšle Pydantic dans votre test et que vous souhaitez envoyer ses donnĂ©es Ă lâapplication pendant les tests, vous pouvez utiliser le jsonable_encoder dĂ©crit dans Encodeur compatible JSON.
ExĂ©cuter¶
AprĂšs cela, vous avez simplement besoin dâinstaller pytest.
Vous devez crĂ©er un environnement virtuel, lâactiver, puis y installer le paquet, par exemple :
$ pip install pytest
---> 100%
Il détectera automatiquement les fichiers et les tests, les exécutera et vous communiquera les résultats.
Exécutez les tests avec :
$ 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>