テスト¶
Starlette のおかげで、FastAPI アプリケーションのテストは簡単で楽しいものになっています。
HTTPX がベースなので、非常に使いやすく直感的です。
これを使用すると、FastAPI と共に pytest を直接利用できます。
TestClient
を使用¶
TestClient
をインポートします。
TestClient
を作成し、FastAPI に渡します。
test_
から始まる名前の関数を作成します (これは pytest
の標準的なコンベンションです)。
httpx
と同じ様に TestClient
オブジェクトを使用します。
チェックしたい Python の標準的な式と共に、シンプルに assert
文を記述します。
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"}
豆知識
テスト関数は async def
ではなく、通常の def
であることに注意してください。
また、クライアントへの呼び出しも通常の呼び出しであり、await
を使用しません。
これにより、煩雑にならずに、pytest
を直接使用できます。
技術詳細
from starlette.testclient import TestClient
も使用できます。
FastAPI は開発者の利便性のために fastapi.testclient
と同じ starlette.testclient
を提供します。しかし、実際にはStarletteから直接渡されています。
豆知識
FastAPIアプリケーションへのリクエストの送信とは別に、テストで async
関数 (非同期データベース関数など) を呼び出したい場合は、高度なチュートリアルのAsync Tests を参照してください。
テストの分離¶
実際のアプリケーションでは、おそらくテストを別のファイルに保存します。
また、FastAPI アプリケーションは、複数のファイル/モジュールなどで構成されている場合もあります。
FastAPI アプリファイル¶
FastAPI アプリに main.py
ファイルがあるとします:
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def read_main():
return {"msg": "Hello World"}
テストファイル¶
次に、テストを含む test_main.py
ファイルを作成し、main
モジュール (main.py
) から app
をインポートします:
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"}
テスト: 例の拡張¶
次に、この例を拡張し、詳細を追加して、さまざまなパーツをテストする方法を確認しましょう。
拡張版 FastAPI アプリファイル¶
FastAPI アプリに main_b.py
ファイルがあるとします。
そのファイルには、エラーを返す可能性のある GET
オペレーションがあります。
また、いくつかのエラーを返す可能性のある POST
オペレーションもあります。
これらの path operation には X-Token
ヘッダーが必要です。
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/", response_model=Item)
async def create_item(item: Item, x_token: str = Header()):
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
return item
拡張版テストファイル¶
次に、先程のものに拡張版のテストを加えた、test_main_b.py
を作成します。
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"}
リクエストに情報を渡せるクライアントが必要で、その方法がわからない場合はいつでも、httpx
での実現方法を検索 (Google) できます。
テストでも同じことを行います。
例えば:
- パス または クエリ パラメータを渡すには、それをURL自体に追加します。
- JSONボディを渡すには、Pythonオブジェクト (例:
dict
) をjson
パラメータに渡します。 - JSONの代わりに フォームデータ を送信する必要がある場合は、代わりに
data
パラメータを使用してください。 - ヘッダー を渡すには、
headers
パラメータにdict
を渡します。 - cookies の場合、
cookies
パラメータにdict
です。
(httpx
または TestClient
を使用して) バックエンドにデータを渡す方法の詳細は、HTTPXのドキュメントを確認してください。
情報
TestClient
は、Pydanticモデルではなく、JSONに変換できるデータを受け取ることに注意してください。
テストにPydanticモデルがあり、テスト中にそのデータをアプリケーションに送信したい場合は、JSON互換エンコーダ で説明されている jsonable_encoder
が利用できます。
実行¶
後は、pytest
をインストールするだけです:
$ pip install pytest
---> 100%
ファイルを検知し、自動テストを実行し、結果のレポートを返します。
以下でテストを実行します:
$ 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>