なぜユニットテストだけでは不十分なのか?
APIのコーディングが終わった直後、ユニットテストがパスし、結合テストもスムーズに進むと、大きな自信を感じることがあります。しかし、現実はもっと厳しいものです。テスターが「奇妙な」ペイロードを入力したり、正の数が求められている場所に負の数を入力したりするだけで、サーバーが突然500エラーを吐いてダウンしてしまうことがあります。
問題は、私たちが通常、想定内のケース(ハッピーパス)に対してのみテストを記述することにあります。ロジックエラーを見つけるために、数千ものデータの組み合わせを手動でリストアップするのは不可能です。
実際のFastAPIプロジェクトで、私はデータのバリデーションをPydanticに完全に任せていました。しかし、Schemathesisでスキャンしたところ、limitパラメータの負の数処理にエラーがあり、SQL文がクラッシュすることが即座に判明しました。Schemathesisは、非常に厳しいテスターのような存在です。OpenAPI (Swagger) ファイルに基づいて、システムの弱点を見つけるための「意地悪な」テストケースを何百も自動生成します。
このツールは**プロパティベーステスト(Property-based Testing)**という手法を使用しています。1+1が2になるかどうかを確認するだけでなく、あらゆる整数xとyでテストを行います。目的は、関数が常に正しいデータ型を返し、プログラムをクラッシュさせないことを保証することです。
Schemathesisを10秒でインストールする
開始するには、OpenAPI標準のドキュメント(通常は /openapi.json または /swagger.json のリンク)が用意されたAPIが必要です。FastAPIやFlask-Smorestを使用している場合、これらのフレームワークは自動的にファイルを生成してくれます。
Schemathesisのインストールはpip経由で非常に簡単です:
pip install schemathesis
http://127.0.0.1:8000 でFastAPIアプリケーションが動作していると仮定しましょう。次のコマンドを実行して、その威力を体験してください:
st run http://127.0.0.1:8000/openapi.json
プロジェクトがまだない場合は、テスト用に app.py ファイルを素早く作成してください:
from fastapi import FastAPI
app = FastAPI()
@app.get("/items/{item_id}")
def read_item(item_id: int, q: str = None):
# ロジックエラー:item_idが負の数の場合の処理が抜けている
if item_id < 0:
raise RuntimeError("データベースがクラッシュしました!")
return {"item_id": item_id, "q": q}
uvicorn app:app でアプリを起動した後、st run コマンドを実行すると、item_id = -1 を送信しようとした際に、わずか数秒で500エラーをキャッチします。
実プロジェクト向けの詳細なテスト設定
基本的なCLIコマンドの実行は第一歩に過ぎません。セキュリティエラーやデータの不一致を捉えるには、より厳密な設定が必要です。
1. チェックルールの強化(Checks)
デフォルトでは、Schemathesisは500エラーのみを検索します。APIが設計通りに動作することを保証するために、私は通常以下のフラグを追加します:
st run http://127.0.0.1:8000/openapi.json \
--check not_a_server_error \
--check status_code_conformance \
--check content_type_conformance \
--check response_headers_conformance
- status_code_conformance: 返されるステータスコードが定義したリスト内にあることを保証します。Swaggerで200のみを宣言しているのに404を返した場合、Schemathesisは即座にエラーを報告します。
- content_type_conformance: 返される形式(JSON, XMLなど)がドキュメントの定義通りかどうかをチェックします。
2. 認証の処理
ログインが必要なAPIの場合、実行コマンドに直接ヘッダーを渡すことができます:
st run http://localhost:8000/openapi.json -H "Authorization: Bearer YOUR_TOKEN"
3. Pytestへの直接統合
Pythonスクリプトを作成することで、プロフェッショナルなCI/CDプロセス向けに入力データ生成(Data Generation)をカスタマイズする際の柔軟性が高まります。
import schemathesis
import pytest
schema = schemathesis.from_uri("http://127.0.0.1:8000/openapi.json")
@schema.parametrize()
def test_api(case):
response = case.call()
case.validate_response(response)
大規模プロジェクトでのちょっとしたコツ:--hypothesis-max-examples=100 を使用してください。この数値はテストケースの数を制限し、CIの実行時間が長くなりすぎてパイプラインが詰まるのを防ぎます。
結果の読み取りとエラー処理
Schemathesisで最も気に入っている点は、エラーの再現(Reproduce)機能です。問題が検出されると、デバッグのために即座にコピー&ペーストできる正確な curl コードが出力されます。
レポート結果には通常、Falsifying example(エラーを引き起こす具体的なペイロード)と Stateful Testing が含まれます。ステートフル機能により、「ユーザーを作成してから削除する」といった一連の連続したリクエストを実行でき、データフローに関するロジックエラーを見つけるのに役立ちます。
GitHub Actionsによる自動化
バグのあるコードがマージされるのを防ぐため、私は常にワークフローの厳格なフィルターとしてSchemathesisを設定しています。APIがOpenAPIの定義に違反した場合、ビルドは即座に失敗します。
- name: Run Schemathesis Tests
run: st run http://localhost:8000/openapi.json --exitfirst
私の実践的な教訓は、「手書きのドキュメントを完全に信じないこと」です。SwaggerでStringを返すと書いてあっても、実際にはコードがNullを返すことがあるかもしれません。Schemathesisは、このような不一致を捉えるための最も客観的なツールになります。
Schemathesisをプロセスに導入することで、QAチームは一息つくことができ、開発者もリリースの際により自信を持てるようになります。PythonでREST APIを構築しているなら、一度プロジェクトをスキャンしてみてください。その結果に驚くかもしれません。

