विषय पर बढ़ें

Testing

🌐 एआई और मनुष्यों द्वारा किया गया अनुवाद

यह अनुवाद मनुष्यों के मार्गदर्शन में एआई द्वारा किया गया है। 🤝

इसमें मूल अर्थ को गलत समझने या अप्राकृतिक लगने आदि जैसी गलतियाँ हो सकती हैं। 🤖

आप हमें एआई LLM को बेहतर मार्गदर्शन करने में मदद करके इस अनुवाद को बेहतर बना सकते हैं।

अंग्रेज़ी संस्करण

Starlette की बदौलत, FastAPI applications की testing आसान और आनंददायक है।

यह HTTPX पर आधारित है, जो बदले में Requests के आधार पर design किया गया है, इसलिए यह बहुत परिचित और intuitive है।

इसके साथ, आप FastAPI के साथ सीधे pytest का उपयोग कर सकते हैं।

TestClient का उपयोग करना

नोट

TestClient का उपयोग करने के लिए, पहले httpx install करें।

सुनिश्चित करें कि आप एक virtual environment बनाते हैं, उसे activate करते हैं, और फिर इसे install करते हैं, उदाहरण के लिए:

$ pip install httpx

TestClient import करें।

अपनी FastAPI application को इसमें pass करके एक TestClient बनाएँ।

ऐसी functions बनाएँ जिनका नाम test_ से शुरू होता हो (यह एक standard pytest convention है)।

TestClient object का उपयोग उसी तरह करें जैसे आप httpx के साथ करते हैं।

जिन चीज़ों की आपको जाँच करनी है उनके लिए standard Python expressions के साथ सरल assert statements लिखें (फिर से, 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"}

टिप

ध्यान दें कि testing functions सामान्य def हैं, async def नहीं।

और client को की जाने वाली calls भी सामान्य calls हैं, await का उपयोग नहीं करतीं।

यह आपको बिना जटिलताओं के सीधे pytest का उपयोग करने देता है।

तकनीकी विवरण

आप from starlette.testclient import TestClient का भी उपयोग कर सकते हैं।

FastAPI आपकी सुविधा के लिए, developer के रूप में, उसी starlette.testclient को fastapi.testclient के रूप में प्रदान करता है। लेकिन यह सीधे Starlette से आता है।

टिप

अगर आप अपनी FastAPI application को requests भेजने के अलावा अपने tests में async functions call करना चाहते हैं (जैसे asynchronous database functions), तो advanced tutorial में Async Tests देखें।

Tests को अलग करना

एक वास्तविक application में, आपके tests शायद किसी अलग file में होंगे।

और आपकी FastAPI application भी कई files/modules आदि से मिलकर बनी हो सकती है।

FastAPI app file

मान लीजिए आपके पास Bigger Applications में बताए गए अनुसार एक file structure है:

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

main.py file में आपकी FastAPI app है:

from fastapi import FastAPI

app = FastAPI()


@app.get("/")
async def read_main():
    return {"msg": "Hello World"}

Testing file

फिर आपके पास अपने tests के साथ एक file test_main.py हो सकती है। यह उसी Python package में हो सकती है (वही directory जिसमें __init__.py file है):

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

क्योंकि यह file उसी package में है, आप main module (main.py) से object app को import करने के लिए relative imports का उपयोग कर सकते हैं:

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"}

...और tests के लिए code पहले जैसा ही रख सकते हैं।

Testing: विस्तृत उदाहरण

अब इस उदाहरण को आगे बढ़ाते हैं और अलग-अलग हिस्सों की testing कैसे करनी है यह देखने के लिए और विवरण जोड़ते हैं।

विस्तारित FastAPI app file

आइए पहले जैसी ही file structure के साथ जारी रखें:

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

मान लीजिए अब आपकी FastAPI app वाली file main.py में कुछ अन्य path operations हैं।

इसमें एक GET operation है जो error return कर सकता है।

इसमें एक POST operation है जो कई errors return कर सकता है।

दोनों path operations को X-Token header required है।

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

विस्तारित testing file

फिर आप विस्तारित tests के साथ test_main.py को update कर सकते हैं:

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"}

जब भी आपको client से request में जानकारी pass करवानी हो और आपको पता न हो कि कैसे, तो आप खोज (Google) सकते हैं कि इसे httpx में कैसे करें, या यहाँ तक कि requests के साथ कैसे करें, क्योंकि HTTPX का design Requests के design पर आधारित है।

फिर आप अपने tests में वही करते हैं।

उदाहरण के लिए:

  • path या query parameter pass करने के लिए, इसे URL में ही जोड़ें।
  • JSON body pass करने के लिए, json parameter में एक Python object (जैसे dict) pass करें।
  • अगर आपको JSON के बजाय Form Data भेजना है, तो इसके बजाय data parameter का उपयोग करें।
  • headers pass करने के लिए, headers parameter में एक dict का उपयोग करें।
  • cookies के लिए, cookies parameter में एक dict

backend को data कैसे pass करें (httpx या TestClient का उपयोग करके) इसके बारे में अधिक जानकारी के लिए HTTPX documentation देखें।

नोट

ध्यान दें कि TestClient ऐसा data receive करता है जिसे JSON में convert किया जा सकता है, Pydantic models नहीं।

अगर आपके test में एक Pydantic model है और आप testing के दौरान उसका data application को भेजना चाहते हैं, तो आप JSON Compatible Encoder में बताए गए jsonable_encoder का उपयोग कर सकते हैं।

इसे चलाएँ

उसके बाद, आपको बस pytest install करना है।

सुनिश्चित करें कि आप एक virtual environment बनाते हैं, उसे activate करते हैं, और फिर इसे install करते हैं, उदाहरण के लिए:

$ pip install pytest

---> 100%

यह files और tests को automatically detect करेगा, उन्हें execute करेगा, और results आपको वापस report करेगा।

Tests चलाएँ:

$ 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>