Thanks to visit codestin.com
Credit goes to fastapi.tiangolo.com

Aller au contenu

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.

Version anglaise

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

Remarque

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ètre json.
  • 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 dict dans le paramètre headers.
  • Pour les cookies, un dict dans le paramètre cookies.

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.

Remarque

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>