Ana içeriğe geç

Test Etme

🌐 Yapay Zekâ ve İnsanlar Tarafından Çeviri

Bu çeviri, insanlar tarafından yönlendirilen bir yapay zekâ ile oluşturuldu. 🤝

Orijinal anlamın yanlış anlaşılması ya da kulağa doğal gelmeme gibi hatalar içerebilir. 🤖

Yapay zekâ LLM'ini daha iyi yönlendirmemize yardımcı olarak bu çeviriyi iyileştirebilirsiniz.

İngilizce sürüm

Starlette sayesinde FastAPI uygulamalarını test etmek kolay ve keyiflidir.

Temelde HTTPX üzerine kuruludur; HTTPX de Requests’i temel alarak tasarlandığı için oldukça tanıdık ve sezgiseldir.

Bununla birlikte FastAPI ile pytest'i doğrudan kullanabilirsiniz.

TestClient Kullanımı

Bilgi

TestClient kullanmak için önce httpx'i kurun.

Bir Sanal Ortam oluşturduğunuzdan, onu aktifleştirdiğinizden ve sonra kurulumu yaptığınızdan emin olun; örneğin:

$ pip install httpx

TestClient'ı import edin.

FastAPI uygulamanızı ona vererek bir TestClient oluşturun.

Adı test_ ile başlayan fonksiyonlar oluşturun (bu, pytest'in standart konvansiyonudur).

TestClient nesnesini httpx ile kullandığınız şekilde kullanın.

Kontrol etmeniz gereken şeyler için standart Python ifadeleriyle basit assert satırları yazın (bu da pytest standardıdır).

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

İpucu

Test fonksiyonlarının async def değil, normal def olduğuna dikkat edin.

Client'a yapılan çağrılar da await kullanılmadan, normal çağrılardır.

Bu sayede pytest'i ek bir karmaşıklık olmadan doğrudan kullanabilirsiniz.

Teknik Detaylar

İsterseniz from starlette.testclient import TestClient da kullanabilirsiniz.

FastAPI, geliştirici olarak size kolaylık olsun diye starlette.testclientfastapi.testclient üzerinden de sunar. Ancak asıl kaynak doğrudan Starlette'tır.

İpucu

FastAPI uygulamanıza request göndermenin dışında testlerinizde async fonksiyonlar çağırmak istiyorsanız (örn. asenkron veritabanı fonksiyonları), ileri seviye bölümdeki Asenkron Testler dokümanına göz atın.

Testleri Ayırma

Gerçek bir uygulamada testlerinizi büyük ihtimalle farklı bir dosyada tutarsınız.

Ayrıca FastAPI uygulamanız birden fazla dosya/modül vb. ile de oluşturulmuş olabilir.

FastAPI Uygulama Dosyası

Daha Büyük Uygulamalar'te anlatılan şekilde bir dosya yapınız olduğunu varsayalım:

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

main.py dosyasında FastAPI uygulamanız bulunuyor olsun:

from fastapi import FastAPI

app = FastAPI()


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

Test Dosyası

Sonra testlerinizin olduğu bir test_main.py dosyanız olabilir. Bu dosya aynı Python package içinde (yani __init__.py dosyası olan aynı dizinde) durabilir:

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

Bu dosya aynı package içinde olduğu için, main modülünden (main.py) app nesnesini import etmek üzere relative import kullanabilirsiniz:

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

...ve test kodunu da öncekiyle aynı şekilde yazabilirsiniz.

Test Etme: Genişletilmiş Örnek

Şimdi bu örneği genişletelim ve farklı parçaların nasıl test edildiğini görmek için daha fazla detay ekleyelim.

Genişletilmiş FastAPI Uygulama Dosyası

Aynı dosya yapısıyla devam edelim:

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

Diyelim ki FastAPI uygulamanızın bulunduğu main.py dosyasında artık başka path operations da var.

Hata döndürebilecek bir GET operation'ı var.

Birden fazla farklı hata döndürebilecek bir POST operation'ı var.

Her iki path operation da X-Token header'ını gerektiriyor.

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

Genişletilmiş Test Dosyası

Sonrasında test_main.py dosyanızı genişletilmiş testlerle güncelleyebilirsiniz:

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'ın request içinde bir bilgi göndermesi gerektiğinde ve bunu nasıl yapacağınızı bilemediğinizde, httpx ile nasıl yapılacağını aratabilirsiniz (Google) ya da HTTPX’in tasarımı Requests’e dayandığı için requests ile nasıl yapıldığını da arayabilirsiniz.

Sonra testlerinizde aynısını uygularsınız.

Örn.:

  • Bir path veya query parametresi geçirmek için, URL’nin kendisine ekleyin.
  • JSON body göndermek için, json parametresine bir Python nesnesi (örn. bir dict) verin.
  • JSON yerine Form Data göndermeniz gerekiyorsa, bunun yerine data parametresini kullanın.
  • headers göndermek için, headers parametresine bir dict verin.
  • cookies için, cookies parametresine bir dict verin.

Backend'e veri geçme hakkında daha fazla bilgi için (httpx veya TestClient kullanarak) HTTPX dokümantasyonu'na bakın.

Bilgi

TestClient'ın Pydantic model'lerini değil, JSON'a dönüştürülebilen verileri aldığını unutmayın.

Testinizde bir Pydantic model'iniz varsa ve test sırasında verisini uygulamaya göndermek istiyorsanız, JSON Uyumlu Encoder içinde açıklanan jsonable_encoder'ı kullanabilirsiniz.

Çalıştırma

Bundan sonra yapmanız gereken tek şey pytest'i kurmaktır.

Bir Sanal Ortam oluşturduğunuzdan, onu aktifleştirdiğinizden ve sonra kurulumu yaptığınızdan emin olun; örneğin:

$ pip install pytest

---> 100%

Dosyaları ve testleri otomatik olarak bulur, çalıştırır ve sonuçları size raporlar.

Testleri şu şekilde çalıştırın:

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