Day 74:Web APIテスト ― 「ちゃんと動くか?をコードで確かめる」
Day 74では、Web/API開発の中でもとても大事なテーマ、APIテスト を扱います。
キーワードはこの3つです。
- APIテスト
- 正常系
- 異常系
ここまでで、FastAPI+SQLiteでCRUD APIを作り、 認証の基礎にも触れてきました。
今日はそれを一歩進めて、 「本当に期待どおりに動いているか?」を、テストコードで確認する という視点を身につけていきます。
APIテストとは ― 「人間の代わりに、コードが動作確認してくれる」
手動テストと自動テスト
APIを作ったとき、最初にやるのはだいたいこんな感じですよね。
- ブラウザや
curl、HTTPie、Postmanなどでリクエストを送る - レスポンスを目で見て、「お、ちゃんと動いてる」と確認する
これは 手動テスト です。
一方で、自動テスト はこうです。
- 「こういう入力をしたら、こういうレスポンスが返るはず」という期待をコードに書く
- テストを実行すると、コードが勝手にAPIを叩いて、結果をチェックしてくれる
APIテストとは、この 自動テストの世界での「動作確認」 です。
なぜAPIテストが大事なのか
理由はいくつもありますが、特に大きいのはこのあたりです。
- 変更を加えたときに、「前に動いていたところが壊れていないか」を自動で確認できる
- チーム開発で、「このAPIはこういう振る舞いをする」という仕様をテストコードとして共有できる
- バグが出たときに、「再発防止のためのテスト」を追加しておける
つまり、APIテストは 「安心して変更できるための土台」 になります。
テストの基本セットアップ ― FastAPI+TestClient+pytest
必要なライブラリ
FastAPIのAPIテストでは、よく次の組み合わせが使われます。
- pytest:Pythonのテストフレームワーク
- TestClient(
fastapi.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で触れたような、 小さくてシンプルなテストからで構わないので、 少しずつ 「コードで確認する習慣」 を育てていくと、 開発の安心感が、じわじわと、でも確実に増していきます。
テストは、 「自分を疑うためのもの」ではなく、 「自分のコードを信じるための味方」 だと、 どこかでふっと感じられるようになるはずです。
