基礎から学ぶPython入門 90日コース | データベース・Web API - Day 74:Web APIテスト

Python 90日で身につけるPython
スポンサーリンク
スポンサーリンク

Day 74:Web APIテスト ― 「ちゃんと動くか?をコードで確かめる」

Day 74では、Web/API開発の中でもとても大事なテーマ、APIテスト を扱います。

キーワードはこの3つです。

  • APIテスト
  • 正常系
  • 異常系

ここまでで、FastAPI+SQLiteでCRUD APIを作り、 認証の基礎にも触れてきました。

今日はそれを一歩進めて、 「本当に期待どおりに動いているか?」を、テストコードで確認する という視点を身につけていきます。

APIテストとは ― 「人間の代わりに、コードが動作確認してくれる」

手動テストと自動テスト

APIを作ったとき、最初にやるのはだいたいこんな感じですよね。

  • ブラウザや curlHTTPiePostman などでリクエストを送る
  • レスポンスを目で見て、「お、ちゃんと動いてる」と確認する

これは 手動テスト です。

一方で、自動テスト はこうです。

  • 「こういう入力をしたら、こういうレスポンスが返るはず」という期待をコードに書く
  • テストを実行すると、コードが勝手にAPIを叩いて、結果をチェックしてくれる

APIテストとは、この 自動テストの世界での「動作確認」 です。

なぜAPIテストが大事なのか

理由はいくつもありますが、特に大きいのはこのあたりです。

  • 変更を加えたときに、「前に動いていたところが壊れていないか」を自動で確認できる
  • チーム開発で、「このAPIはこういう振る舞いをする」という仕様をテストコードとして共有できる
  • バグが出たときに、「再発防止のためのテスト」を追加しておける

つまり、APIテストは 「安心して変更できるための土台」 になります。

テストの基本セットアップ ― FastAPI+TestClient+pytest

必要なライブラリ

FastAPIのAPIテストでは、よく次の組み合わせが使われます。

  • pytest:Pythonのテストフレームワーク
  • TestClientfastapi.testclient):FastAPIアプリをテスト用に叩くためのクライアント

インストール例:

bash

pip install pytest

(FastAPIをすでに入れていれば、TestClient は使えます)

シンプルなAPIを用意する

まずは、テスト対象となる、とてもシンプルなAPIを用意します。

# day74_app.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    price: int


items: dict[int, Item] = {}
next_id: int = 1


@app.get("/")
def read_root():
    """
    簡単なウェルカムメッセージを返すAPIです。
    テストの最初の対象にしやすいエンドポイントです。
    """
    return {"message": "Day 74: Web APIテストへようこそ"}


@app.post("/items")
def create_item(item: Item):
    """
    新しい商品を作成するAPIです。
    - リクエストボディとして Item を受け取ります。
    - メモリ上の辞書に保存します。
    - 作成された商品とIDを返します。
    """

    global next_id

    if item.price < 0:
        # 価格がマイナスの場合は異常系として扱い、400を返します。
        raise HTTPException(status_code=400, detail="Price must be non-negative")

    items[next_id] = item
    created = {"id": next_id, "item": item}
    next_id += 1

    return created


@app.get("/items/{item_id}")
def get_item(item_id: int):
    """
    指定したIDの商品を取得するAPIです。
    - 存在しないIDの場合は404を返します。
    """

    item = items.get(item_id)
    if item is None:
        raise HTTPException(status_code=404, detail="Item not found")

    return {"id": item_id, "item": item}
Python

この小さなAPIを使って、 正常系異常系 のテストを書いていきます。

正常系テスト ― 「期待どおりに成功するか」を確認する

正常系って何?

正常系 とは、

「正しい入力をしたときに、期待どおりの成功レスポンスが返るか」

を確認するテストです。

例えば、

  • 正しい商品データを送ったら、ちゃんと作成されるか
  • 作成した商品を取得したら、同じ内容が返ってくるか

などです。

TestClientを使った基本的なテスト

テストコードは、別ファイルに書きます。

# test_day74_app_normal.py
from fastapi.testclient import TestClient
from day74_app import app

# FastAPIアプリをテスト用クライアントで包みます。
client = TestClient(app)


def test_read_root_normal():
    """
    正常系:トップエンドポイントが 200 でメッセージを返すかを確認します。
    """

    response = client.get("/")
    assert response.status_code == 200
    data = response.json()
    assert "message" in data
    assert "Day 74" in data["message"]


def test_create_item_normal():
    """
    正常系:正しい商品データを送ったときに、商品が作成されるかを確認します。
    """

    payload = {"name": "Apple", "price": 120}

    response = client.post("/items", json=payload)
    assert response.status_code == 200

    data = response.json()
    assert "id" in data
    assert "item" in data
    assert data["item"]["name"] == "Apple"
    assert data["item"]["price"] == 120


def test_get_item_normal():
    """
    正常系:事前に商品を作成し、そのIDで取得できるかを確認します。
    """

    # まず商品を作成します。
    payload = {"name": "Banana", "price": 80}
    create_response = client.post("/items", json=payload)
    assert create_response.status_code == 200
    created = create_response.json()
    item_id = created["id"]

    # 次に、そのIDで商品を取得します。
    get_response = client.get(f"/items/{item_id}")
    assert get_response.status_code == 200

    data = get_response.json()
    assert data["id"] == item_id
    assert data["item"]["name"] == "Banana"
    assert data["item"]["price"] == 80
Python

ここでやっていることは、とてもシンプルです。

  • client.get()client.post() でAPIを叩く
  • status_code が期待どおりか確認する
  • response.json() で中身を取り出し、フィールドや値をチェックする

これだけでも、「ちゃんと成功するか?」をコードで確認できる ようになります。

異常系テスト ― 「おかしな入力のときに、ちゃんと失敗してくれるか」を確認する

異常系って何?

異常系 とは、

「おかしな入力や、ありえない状況のときに、適切なエラーを返すか」

を確認するテストです。

例えば、

  • 価格がマイナスのときに、400 Bad Request を返しているか
  • 存在しないIDで商品を取得しようとしたときに、404 Not Found を返しているか

などです。

異常系テストは、 「壊れないか?」ではなく「ちゃんと拒否してくれるか?」 を見るテストです。

異常系テストの例

# test_day74_app_error.py
from fastapi.testclient import TestClient
from day74_app import app

client = TestClient(app)


def test_create_item_negative_price():
    """
    異常系:価格がマイナスのときに 400 が返るかを確認します。
    """

    payload = {"name": "InvalidItem", "price": -10}

    response = client.post("/items", json=payload)
    assert response.status_code == 400

    data = response.json()
    assert "detail" in data
    assert "Price must be non-negative" in data["detail"]


def test_get_item_not_found():
    """
    異常系:存在しないIDの商品を取得しようとしたときに 404 が返るかを確認します。
    """

    response = client.get("/items/99999")
    assert response.status_code == 404

    data = response.json()
    assert "detail" in data
    assert "Item not found" in data["detail"]
Python

ここでのポイントは、

  • ステータスコードが期待どおりの「エラーコード」になっているか
  • エラーメッセージ(detail)が、意味のある内容になっているか

を確認しているところです。

異常系テストは、 「失敗したときの振る舞い」を明らかにしてくれるので、 APIの信頼性をぐっと高めてくれます。

正常系と異常系をセットで考える ― 「成功パターンと失敗パターンの両方が仕様」

仕様としてのテスト

APIの仕様を考えるとき、

  • 「こういう入力なら成功する」
  • 「こういう入力なら失敗する」

の両方が、仕様の一部 です。

テストコードは、その仕様を 「機械が読める形で書いたもの」 とも言えます。

例えば、create_item の仕様をテストから読み解くと、

  • name が文字列で、price が0以上の整数なら成功する
  • price が負の値なら、400と "Price must be non-negative" を返す

ということが分かります。

このように、

  • 正常系テスト → 「こういうときに成功する」
  • 異常系テスト → 「こういうときに失敗する」

をセットで書いておくと、 APIの振る舞いがとてもクリアになります。

Day 74ミニテンプレート ― 正常系+異常系を含んだAPIテスト一式

最後に、今日の内容をひとまとめにした APIテストのミニテンプレート を載せておきます。

テスト対象のアプリ(再掲)

# day74_app_template.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    price: int


items: dict[int, Item] = {}
next_id: int = 1


@app.get("/")
def read_root():
    return {"message": "Day 74: Web APIテストサンプル"}


@app.post("/items")
def create_item(item: Item):
    if item.price < 0:
        raise HTTPException(status_code=400, detail="Price must be non-negative")

    global next_id
    items[next_id] = item
    created = {"id": next_id, "item": item}
    next_id += 1
    return created


@app.get("/items/{item_id}")
def get_item(item_id: int):
    item = items.get(item_id)
    if item is None:
        raise HTTPException(status_code=404, detail="Item not found")

    return {"id": item_id, "item": item}
Python

正常系テスト

# test_day74_normal_template.py
from fastapi.testclient import TestClient
from day74_app_template import app

client = TestClient(app)


def test_read_root_normal():
    response = client.get("/")
    assert response.status_code == 200
    data = response.json()
    assert "message" in data
    assert "Web APIテスト" in data["message"]


def test_create_item_normal():
    payload = {"name": "Orange", "price": 150}
    response = client.post("/items", json=payload)
    assert response.status_code == 200

    data = response.json()
    assert "id" in data
    assert "item" in data
    assert data["item"]["name"] == "Orange"
    assert data["item"]["price"] == 150


def test_get_item_normal():
    payload = {"name": "Grape", "price": 200}
    create_response = client.post("/items", json=payload)
    assert create_response.status_code == 200
    created = create_response.json()
    item_id = created["id"]

    get_response = client.get(f"/items/{item_id}")
    assert get_response.status_code == 200

    data = get_response.json()
    assert data["id"] == item_id
    assert data["item"]["name"] == "Grape"
    assert data["item"]["price"] == 200
Python

異常系テスト

# test_day74_error_template.py
from fastapi.testclient import TestClient
from day74_app_template import app

client = TestClient(app)


def test_create_item_negative_price_error():
    payload = {"name": "BadItem", "price": -1}
    response = client.post("/items", json=payload)
    assert response.status_code == 400

    data = response.json()
    assert "detail" in data
    assert "Price must be non-negative" in data["detail"]


def test_get_item_not_found_error():
    response = client.get("/items/9999")
    assert response.status_code == 404

    data = response.json()
    assert "detail" in data
    assert "Item not found" in data["detail"]
Python

テストの実行は、とてもシンプルです。

bash

pytest

と打てば、 test_ から始まるファイル・関数が自動的に実行され、 成功・失敗が一覧で表示されます。

Day 74のまとめ ― 「テストは、“壊れていない”ことを信じるための味方」

今日の主役は、

  • APIテスト:人間の代わりに、コードがAPIを叩いて動作確認してくれる仕組み
  • 正常系:正しい入力のときに、期待どおりに成功するかを見るテスト
  • 異常系:おかしな入力のときに、ちゃんと失敗してくれるかを見るテスト

でした。

テストというと、

  • 「面倒そう」
  • 「後回しにしたくなる」

という気持ちがどうしても出てきます。

でも、APIが増え、機能が増え、 コードが複雑になっていくほど、 「ちゃんと動いているか?」を自分の記憶だけに頼るのは、かなりしんどくなります。

Day 74で触れたような、 小さくてシンプルなテストからで構わないので、 少しずつ 「コードで確認する習慣」 を育てていくと、 開発の安心感が、じわじわと、でも確実に増していきます。

テストは、 「自分を疑うためのもの」ではなく、 「自分のコードを信じるための味方」 だと、 どこかでふっと感じられるようになるはずです。

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