信頼性の高いアプリケーションを作る上で、自動化されたテストの整備は重要なプラクティスのひとつ。 その観点において Python の Web アプリケーションフレームワークの FastAPI 1 はユニットテストが書きやすい。 今回は FastAPI を使うプロジェクトで pytest 2 でユニットテストを書く場合にどんな感じになるか書き留めておく。
使った環境は次のとおり。
$ sw_vers ProductName: macOS ProductVersion: 26.3 BuildVersion: 25D125 $ uv --version uv 0.10.6 (Homebrew 2026-02-24) $ uv tree 2>/dev/null helloworld v0.1.0 ├── fastapi v0.133.1 │ ├── annotated-doc v0.0.4 │ ├── pydantic v2.12.5 │ │ ├── annotated-types v0.7.0 │ │ ├── pydantic-core v2.41.5 │ │ │ └── typing-extensions v4.15.0 │ │ ├── typing-extensions v4.15.0 │ │ └── typing-inspection v0.4.2 │ │ └── typing-extensions v4.15.0 │ ├── starlette v0.52.1 │ │ └── anyio v4.12.1 │ │ └── idna v3.11 │ ├── typing-extensions v4.15.0 │ └── typing-inspection v0.4.2 (*) ├── uvicorn v0.41.0 │ ├── click v8.3.1 │ └── h11 v0.16.0 ├── httpx v0.28.1 (group: dev) │ ├── anyio v4.12.1 (*) │ ├── certifi v2026.2.25 │ ├── httpcore v1.0.9 │ │ ├── certifi v2026.2.25 │ │ └── h11 v0.16.0 │ └── idna v3.11 └── pytest v9.0.2 (group: dev) ├── iniconfig v2.3.0 ├── packaging v26.0 ├── pluggy v1.6.0 └── pygments v2.19.2 (*) Package tree already displayed
もくじ
プロジェクトを用意する
まずは FastAPI を使ったプロジェクトを用意する。
プロジェクトは uv 3 で管理することを想定する。
そのためにプロジェクトのメタデータを記述した pyproject.toml を用意する。
プロジェクトのパッケージ名は helloworld にする。
$ cat << 'EOF' > pyproject.toml [project] name = "helloworld" version = "0.1.0" requires-python = ">=3.12" dependencies = [ "fastapi", "uvicorn", ] [dependency-groups] dev = [ "httpx", "pytest", ] [tool.uv] package = true EOF
後ほどテストを書くために、開発用の依存パッケージに pytest と httpx を追加しておく。
次に helloworld パッケージを用意する。
Python のパッケージとは __init__.py という名前のファイルが入ったディレクトリである。
$ mkdir -p helloworld $ touch helloworld/__init__.py
そして FastAPI のアプリケーションを含んだモジュールの main.py を作る。
このアプリケーションはルートのパスに HTTP で GET すると JSON を返す HTTP API を定義している。
$ cat << 'EOF' > helloworld/main.py from fastapi import FastAPI app = FastAPI() @app.get("/") def root_get(): return {"message": "Hello World"} EOF
典型的には、上記のような HTTP API にアプリケーションのロジックを実装していく。 それをフロントエンドから呼び出す構成になるだろう。
動作を確認する
続いて、上記で準備したアプリケーションの動作を確認していく。
パッケージをインストールする。
$ uv sync
ASGI サーバの Uvicorn 4 から FastAPI のアプリケーションを起動する。
$ uv run uvicorn helloworld.main:app --reload
上記を実行すると Uvicorn のサーバがフォアグラウンドで起動する。
別のターミナルを開いて curl で HTTP API を呼び出す。
$ curl http://localhost:8000 {"message":"Hello World"}
上記のレスポンスが返ってくれば、ちゃんと動作することが確認できた。
ユニットテストを書く
次は動作を確認したアプリケーションに対してユニットテストを書く。
まずは tests ディレクトリを用意する。
その下にテスト対象のパッケージの構造に対応する形でパッケージを作っていく。
$ mkdir -p tests/helloworld $ touch tests/helloworld/__init__.py
今回の主眼となるユニットテストを書く。
先ほど書いた main.py に対応するユニットテストとして test_main.py を用意する。
$ cat << 'EOF' > tests/helloworld/test_main.py from fastapi.testclient import TestClient from helloworld.main import app def test_root_get(): """GET / の振る舞いをテストする""" client = TestClient(app) response = client.get("/") assert response.status_code == 200 assert response.json() == {"message": "Hello World"} EOF
上記では、次の行で helloworld パッケージから FastAPI のアプリケーションをインポートしている。
from helloworld.main import app
そして、インポートしたアプリケーションを fastapi.testclient.TestClient をインスタンス化するときに引数として渡している。
client = TestClient(app)
この fastapi.testclient.TestClient のインスタンスを使うことでユニットテストが容易に記述できる。
次の行でルートのパスに HTTP GET したレスポンスを得ている。
response = client.get("/")
そして、得られたレスポンスのステータスコードや中身を検証している。
assert response.status_code == 200 assert response.json() == {"message": "Hello World"}
テストコードは pytest コマンドで実行できる。
$ uv run pytest -v ============================= test session starts ============================== platform darwin -- Python 3.12.12, pytest-9.0.2, pluggy-1.6.0 -- /Users/amedama/Documents/example/.venv/bin/python3 cachedir: .pytest_cache rootdir: /Users/amedama/Documents/example configfile: pyproject.toml plugins: anyio-4.12.1 collected 1 item tests/helloworld/test_main.py::test_root_get PASSED [100%] ============================== 1 passed in 0.10s ===============================
テストがパスした。 つまり、アプリケーションからレスポンスが返って、その検証に成功している。
まとめ
今回は FastAPI を使ったプロジェクトで HTTP API に pytest でユニットテストを書く方法を紹介した。 テストで重要なポイントは、アプリケーションのインターフェイスの振る舞いを検証すること。 HTTP API はフロントエンドとのインターフェイスなのでテストを書く対象として相応しい。
参考までに、ディレクトリ構成は次のようになる。
$ tree .
.
├── helloworld
│ ├── __init__.py
│ └── main.py
├── pyproject.toml
└── tests
└── helloworld
├── __init__.py
└── test_main.py
4 directories, 5 files
