【第1回】Python pytest 入門 — FastAPIアプリのテスト環境をゼロから構築しよう

プログラミング

「テスト書いた方がいいのはわかってるんだけど、何から始めればいいのかが全然わからない」——自分もずっとそれでした。FastAPIでAPIを作るのは楽しいのに、テストの話になったとたんに急に腰が重くなる感じ、ありませんか。

このシリーズでは、FastAPIで作ったAPIを題材にして、pytestによるテスト設計をゼロから学んでいきます。全4回構成で、今回の第1回は「環境構築と基本的なテストの書き方」に絞った内容です。難しいことは後回しにして、まずは動かせるところまでいきましょう。

  • 第1回(本記事):環境構築とはじめてのpytest
  • 第2回:fixtureを使ったテストの共通化
  • 第3回:モック・パッチでDBや外部APIを切り離す
  • 第4回:カバレッジ計測とCI(GitHub Actions)への組み込み

この記事でわかること

  • FastAPI×pytestの開発環境をセットアップする方法
  • TestClientを使ったテストコードの基本的な書き方
  • pyproject.tomlでpytest設定をまとめる方法
  • テスト設計で気をつけるべき基本的なポイント

前提・バージョン情報

本記事執筆時点(2026年05月)のバージョンは以下のとおりです。

  • Python: 3.12系
  • fastapi[standard](0.115系)
  • pytest: 9.0系
  • httpx: 0.28系

pytestはv8系からv9系へのメジャーアップデートで変更点があるので、古い記事を参照するときは注意が必要です。

プロジェクトの構成を作る

まずはシンプルなディレクトリ構成から始めます。本格的なアプリを想定して、最初からテスト用ディレクトリを分けておくのがおすすめです。

myapi/
├── app/
│   ├── __init__.py
│   ├── main.py
│   └── routers/
│       ├── __init__.py
│       └── items.py
├── tests/
│   ├── __init__.py
│   └── test_items.py
├── pyproject.toml
└── requirements.txt

アプリ本体とテストを分けておくと、後でカバレッジを測るときも管理がしやすいです。

パッケージのインストール

仮想環境を作って必要なものを入れます。

python -m venv .venv
source .venv/bin/activate  # Windowsは .venv\Scripts\activate

pip install "fastapi[standard]" pytest httpx

fastapi[standard] は「標準的な依存関係込みで入れる」ためのインストール方法です。httpx はTestClientがHTTPXベースで動作するため、一緒に入れておくと安心です。

uvを使っている場合は uv add "fastapi[standard]" pytest httpx でOK。非同期テストを書く場合はさらに pytest-asyncio を追加インストールしておくと便利です。今回はまだ使いませんが、頭の片隅に置いておくといいかもしれません。

題材にするFastAPIアプリを用意する

テスト対象のアプリをサクッと作ります。今回は「アイテムを取得・登録するシンプルなREST API」を題材にします。

app/main.py

from fastapi import FastAPI
from app.routers import items

app = FastAPI(title="MyAPI")
app.include_router(items.router)

app/routers/items.py

from fastapi import APIRouter, HTTPException
from pydantic import BaseModel

router = APIRouter(prefix="/items", tags=["items"])

# インメモリのデータストア(テスト用にシンプルに)
db: dict[int, dict] = {}
_next_id = 1


class ItemCreate(BaseModel):
    name: str
    price: float


class ItemResponse(BaseModel):
    id: int
    name: str
    price: float


@router.get("/{item_id}", response_model=ItemResponse)
def get_item(item_id: int):
    if item_id not in db:
        raise HTTPException(status_code=404, detail="Item not found")
    return {"id": item_id, **db[item_id]}


@router.post("/", response_model=ItemResponse, status_code=201)
def create_item(item: ItemCreate):
    global _next_id
    db[_next_id] = {"name": item.name, "price": item.price}
    result = {"id": _next_id, **db[_next_id]}
    _next_id += 1
    return result

グローバル変数でデータを持つのはお行儀が悪いですが、最初はこれで十分です。後の回でDBに差し替えます。

はじめてのpytestを書く

いよいよ本題。FastAPIのテストで中心になるのが TestClient というクラスです。

FastAPIではTestClientが用意されていて、これを使うことでサーバーを起動しなくても、FastAPIのコードと直接やり取りしてアプリをテストできます。実行速度も速いので、テストのサイクルが短くなるのが地味に嬉しい。

tests/test_items.py

from fastapi.testclient import TestClient
from app.main import app

client = TestClient(app)


def test_create_item():
    response = client.post("/items/", json={"name": "Apple", "price": 120.0})
    assert response.status_code == 201
    data = response.json()
    assert data["name"] == "Apple"
    assert data["price"] == 120.0
    assert "id" in data


def test_get_item():
    # まず作る
    create_res = client.post("/items/", json={"name": "Banana", "price": 80.0})
    item_id = create_res.json()["id"]

    # 取得する
    response = client.get(f"/items/{item_id}")
    assert response.status_code == 200
    assert response.json()["name"] == "Banana"


def test_get_item_not_found():
    response = client.get("/items/99999")
    assert response.status_code == 404
    assert response.json()["detail"] == "Item not found"

test_ で始まる関数を作ること、TestClientでHTTPリクエストを送ること、そして assert 文で値を検証すること——これがpytestの基本的なスタイルです。

実行してみる

pytest -v

-v をつけると各テストの結果が1行ずつ出るので、何がパスして何が落ちたかが一目でわかります。うまくいけばこんな出力になるはずです:

========================= test session starts ==========================
platform darwin -- Python 3.12.x, pytest-9.0.2, pluggy-1.6.0
collected 3 items

tests/test_items.py::test_create_item PASSED                    [ 33%]
tests/test_items.py::test_get_item PASSED                       [ 67%]
tests/test_items.py::test_get_item_not_found PASSED             [100%]

========================== 3 passed in 0.42s ===========================

3件パスしたらOKです。

ちょっと待って——テスト間でデータが引き継がれる問題

上のコードには実は問題があります。グローバルなインメモリDBを使っているので、テストを実行する順番によって結果が変わることがあります。

test_get_item の中でアイテムを作っていますが、その前に別のテストがアイテムを作っていたら item_id の値が変わってしまいます。今は3件しかないのでたまたま動いていますが、テストが増えてくると必ず壊れます。

これを解決するのが fixture という仕組みで、次回(第2回)で詳しくやります。とりあえず今回はこの問題があることを認識しておけばOKです。最初にこの問題に気づかずにテストを書き続けて、あとから全部直す羽目になったこともあるので、テスト間の独立性を保つことが後々の自分のためになります。

pyproject.toml でpytestの設定をまとめる

地味に便利なのが、pyproject.toml にpytestの設定を書いておくことです。毎回オプションを打つのが面倒なので。

[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-v --tb=short"

testpaths でテストを探す場所を限定して、addopts で毎回つけたいオプションをデフォルトにします。--tb=short はエラー時のトレースバックを短くしてくれるオプションで、長いスタックトレースが流れてくると何が起きたかわかりにくくなるので重宝しています。

この設定を入れると、以降は pytest と打つだけで、概ね pytest -v --tb=short と同じように動きます。pytestはファイルとテストを自動的に検出・実行するので、設定ファイルと合わせるとコマンドがかなりシンプルになります。

※この記事にはプロモーションが含まれます

ちなみに、お名前.com レンタルサーバー(WordPressに特化した高速レンタルサーバー。月額990円〜、独自ドメイン実質0円)も気になっています。お名前.com レンタルサーバー

まとめ

第1回では、FastAPIアプリのテスト環境を構築して、TestClientを使った基本的なテストを動かすところまでやりました。思ったよりシンプルに書けるな、という感覚が伝わっていれば嬉しいです。

次回の第2回では、fixtureと conftest.py を使ってテスト間のデータ汚染を防ぎながら共通処理をまとめる方法をやります。ここがpytestで個人的に一番「おもしろい」と感じた部分です。

📚 シリーズ「Python pytest 入門 — FastAPIアプリを題材にテスト設計をゼロから学ぶ」(第1回 / 全4回)

→ 次回の記事: 公開後にリンクが追加されます

参考になったらクリックしてもらえると嬉しいです!

Blogmura ProgrammingProgramming Ranking
タイトルとURLをコピーしました