Schemathesis:OpenAPI標準でPython APIの自動テストを徹底攻略する

Python tutorial - IT technology blog
Python tutorial - IT technology blog

なぜユニットテストだけでは不十分なのか?

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を構築しているなら、一度プロジェクトをスキャンしてみてください。その結果に驚くかもしれません。

Share: