CUBE SUGAR CONTAINER

技術系のこと書きます。

Python: FastAPI を使ったプロジェクトで HTTP API にユニットテストを書く

信頼性の高いアプリケーションを作る上で、自動化されたテストの整備は重要なプラクティスのひとつ。 その観点において 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

後ほどテストを書くために、開発用の依存パッケージに pytesthttpx を追加しておく。

次に 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