nikkie-ftnextの日記

イベントレポートや読書メモを発信

FastAPIで作るアプリケーションのテストコードを書く(はじめてTestClient)

はじめに

🦁テストコード書いてないのか? nikkieです。

FastAPIとは付き合いがそこそこありますが、ここ最近TestClientを使ったテストを初めて書く機会が訪れました。

目次

ユーザガイド「Testing」

これを見ればOKそうなドキュメントがありました(ありがたや〜)。
こちらに沿って手を動かしていきます。

今回のゴールはHello Worldするエンドポイントのテストとします(初めてなのでハードルは低く!)。
アプリケーションのヘルスチェックのテストになるかなと考えています。

app.py

from fastapi import FastAPI

app = FastAPI()


@app.get("/")
def hello():
    return {"message": "Hello World!"}

動作環境

  • Python 3.12.6
  • python -m pip install 'fastapi[standard]' pytest
    • fastapi 0.115.5
    • pytest 8.3.3

詳細なバージョン

annotated-types==0.7.0
anyio==4.6.2.post1
certifi==2024.8.30
click==8.1.7
dnspython==2.7.0
email_validator==2.2.0
fastapi==0.115.5
fastapi-cli==0.0.5
h11==0.14.0
httpcore==1.0.7
httptools==0.6.4
httpx==0.27.2
idna==3.10
iniconfig==2.0.0
Jinja2==3.1.4
markdown-it-py==3.0.0
MarkupSafe==3.0.2
mdurl==0.1.2
packaging==24.2
pluggy==1.5.0
pydantic==2.10.2
pydantic_core==2.27.1
Pygments==2.18.0
pytest==8.3.3
python-dotenv==1.0.1
python-multipart==0.0.17
PyYAML==6.0.2
rich==13.9.4
shellingham==1.5.4
sniffio==1.3.1
starlette==0.41.3
typer==0.13.1
typing_extensions==4.12.2
uvicorn==0.32.1
uvloop==0.21.0
watchfiles==1.0.0
websockets==14.1

Testingのドキュメントによるとhttpxが必要ですが、pip install fastapi[standard]でインストールされていました。

はじめてTestClient

.
├── app.py
└── test_app.py

test_app.py

from fastapi.testclient import TestClient

from app import app

client = TestClient(app)


def test_ヘルスチェック():
    response = client.get("/")
    assert response.status_code == 200
    assert response.json() == {"message": "Hello World!"}

「そう書くのか〜!」となったポイントは、app.pyにあるappをimportした上でTestClientを初期化した1点。
あとはTestClientインスタンスをHTTPXのように使います!

Use the TestClient object the same way as you do with httpx.

GET /のレスポンスのステータスコードJSONの内容をpytestの書き方で検証しました。

% pytest -v
============================= test session starts ==============================
platform darwin -- Python 3.12.6, pytest-8.3.3, pluggy-1.5.0 -- /.../.venv/bin/python

collected 1 item

test_app.py::test_ヘルスチェック PASSED                                  [100%]

============================== 1 passed in 0.33s ===============================

Bigger Applicationのファイル配置でテスト

Testingのドキュメントの中の「Separating tests」より。
アプリケーションのパッケージにテストコードも配置するんですね!

.
└── app/
    ├── __init__.py
    ├── main.py
    └── test_main.py
  • app/__init__.pyappをパッケージ化
  • app/main.pyでアプリケーションを実装
  • app/test_main.pyがテストコード

「Bigger Applications - Multiple Files」のドキュメントには

appのサブパッケージとして、routersinternalが導入されています。

上記のヘルスチェックのコードをappパッケージに移動してみたところ、appのimportを相対importに変えるだけ2でテストは通り続けました。

from fastapi.testclient import TestClient

-from app import app
+from .main import app

client = TestClient(app)


def test_ヘルスチェック():
% pytest -v
============================= test session starts ==============================
platform darwin -- Python 3.12.6, pytest-8.3.3, pluggy-1.5.0 -- /.../.venv/bin/python

collected 1 item

app/test_main.py::test_ヘルスチェック PASSED                             [100%]

============================== 1 passed in 0.29s ===============================

ちなみにpytestコマンドでapp/test_app.pyの中のテストが見つかるのは、

  • (testpaths未設定なので)現在のディレクトリからテストの収集を始める
  • ディレクトリの中を再帰的に
  • test_*.py または *_test.py という名前のファイルを探す
    • 👉 app/test_app.pyが見つかりますね
  • testで名前が始まる関数やメソッドをテストアイテムとして収集
    • 👉 test_ヘルスチェック

という仕組みによると理解しています。
ref: https://docs.pytest.org/en/stable/explanation/goodpractices.html#conventions-for-python-test-discovery

終わりに

FastAPI製アプリのテストコードを初めて書きました。

  • アプリケーションをimportしてTestClientに渡して初期化
  • TestClientはHTTPXだと思って使えばよい
  • テストコードはアプリケーションの中に配置される(通常のプロジェクトとの違いだな〜)

手元のアプリケーションはまだBiggerではありませんが、テストコードを書きつつ育てていけたらと思っています。


  1. Testingのドキュメントより「Create a TestClient by passing your FastAPI application to it.
  2. Testingのドキュメントのここ:https://fastapi.tiangolo.com/tutorial/testing/#testing-file