テスト¶
🌐 Translation by AI and humans
This translation was made by AI guided by humans. 🤝
It could have mistakes of misunderstanding the original meaning, or looking unnatural, etc. 🤖
You can improve this translation by helping us guide the AI LLM better.
Starlette のおかげで、FastAPI アプリケーションのテストは簡単で楽しいものになっています。
HTTPX がベースで、さらにその設計は Requests をベースにしているため、とても馴染みがあり直感的です。
これを使用すると、FastAPI と共に pytest を直接利用できます。
TestClient を使用¶
情報
TestClient を使用するには、まず httpx をインストールします。
仮想環境 を作成し、それを有効化してから、例えば以下のようにインストールしてください:
$ pip install httpx
TestClient をインポートします。
TestClient を作成し、FastAPI に渡します。
test_ から始まる名前の関数を作成します (これは pytest の標準的なコンベンションです)。
httpx と同じ様に TestClient オブジェクトを使用します。
チェックしたい Python の標準的な式と共に、シンプルに assert 文を記述します (これも 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"}
豆知識
テスト関数は async def ではなく、通常の def であることに注意してください。
また、クライアントへの呼び出しも通常の呼び出しであり、await を使用しません。
これにより、煩雑にならずに、pytest を直接使用できます。
技術詳細
from starlette.testclient import TestClient も使用できます。
FastAPI は開発者の利便性のために fastapi.testclient と同じ starlette.testclient を提供します。しかし、実際にはStarletteから直接渡されています。
豆知識
FastAPIアプリケーションへのリクエストの送信とは別に、テストで async 関数 (非同期データベース関数など) を呼び出したい場合は、高度なチュートリアルのAsync Tests を参照してください。
テストの分離¶
実際のアプリケーションでは、おそらくテストを別のファイルに保存します。
また、FastAPI アプリケーションは、複数のファイル/モジュールなどで構成されている場合もあります。
FastAPI アプリファイル¶
Bigger Applications で説明されている、次のようなファイル構成があるとします:
.
├── app
│ ├── __init__.py
│ └── main.py
ファイル main.py に FastAPI アプリがあります:
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def read_main():
return {"msg": "Hello World"}
テストファイル¶
次に、テストを含む test_main.py ファイルを用意できます。これは同じ Python パッケージ (__init__.py ファイルがある同じディレクトリ) に置けます:
.
├── app
│ ├── __init__.py
│ ├── main.py
│ └── 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 アプリファイル¶
先ほどと同じファイル構成で続けます:
.
├── app
│ ├── __init__.py
│ ├── main.py
│ └── test_main.py
ここで、FastAPI アプリがある main.py ファイルには、他の path operation があります。
エラーを返す可能性のある GET オペレーションがあります。
いくつかのエラーを返す可能性のある POST オペレーションもあります。
両方の path operation には 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
from typing import Annotated, Union
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: Union[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
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
Tip
Prefer to use the Annotated version if possible.
from typing import Union
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: Union[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
拡張版テストファイル¶
次に、拡張版のテストで test_main.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"}
🤓 Other versions and variants
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"}
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"}
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"}
リクエストに情報を渡せるクライアントが必要で、その方法がわからない場合はいつでも、httpx での実現方法、あるいは HTTPX の設計が Requests の設計をベースにしているため requests での実現方法を検索 (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>