筒井
Schemathesisとは何か
Schemathesisは、OpenAPIまたはGraphQLで書かれたAPI仕様を元にテストデータを自動生成し、実際にそれらのテストデータを使ってAPIを呼び出すことで、仕様通りの挙動になっているか検証できるツールです。
Schemathesisを使う利点は以下のとおりです。
- 人間が書いたテストコードでは気付けないエッジケースを見つけられる
- 仕様書と実装の乖離に気づくことができる
Schemathesisの使い方
では、実際にSchemathesisを使ってみましょう。Schemathesisの使い方には、主に以下の2つがあります。
- pytestと組み合わせる方法
- コマンドラインインターフェースから利用する方法
この方法について、以下で順番に説明します。
本記事で使用するPython、Schemathesis、FastAPI、pytestのバージョンは以下のとおりです。
- Python 3.13 
- Schemathesis 4.0.2 
- fastapi[standard] 0.115. 13 
- pytest 8.4.1 
なお、Schemathesisの現在の最新バージョンである4系は、3系とは仕様が大幅に変わりました。詳細な変更点については以下のドキュメントを参照してください。
本記事ではバージョン3系については説明しません。
pytestと組み合わせる方法
Schemathesisは、pytestと組み合わせることで、Schemathesisが生成したテストデータをpytestのテストコードに渡すことができます。
ここでは、FastAPIを使って作成したAPIをテスト対象とする例をお見せします。FastAPIはソースコードを元にOpenAPIスキーマを生成できるので、OpenAPIスキーマの作成は省略します。
アプリケーションの作成とテストの実行
まずは、以下の手順で必要なライブラリをインストールします。
$ python3.13 -m venv .venv $ source .venv/bin/activate (.venv) $ pip install "schemathesis==4.0.2" "fastapi[standard]==0.115.13" "pytest==8.4.1"
次に、以下の内容のmain.a、bを入力値として渡し、a / bの結果を返すシンプルなAPIです。
import fastapi
from pydantic import BaseModel, StrictInt
app = fastapi.FastAPI()
class Values(BaseModel):
    a: StrictInt
    b: StrictInt
@app.post("/div")
async def div(values: Values):
    """2つの整数を受け取り、その商を返すAPIエンドポイント"""
    return {"result": values.a / values.b}
fastapi dev main. でアプリケーションを立ち上げてから、以下のコマンドで動作確認してみます。
$ curl -X 'POST' \
  'http://127.0.0.1:8000/div' \
  -H 'accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
  "a": 10,
  "b": 2
}'
{"result":5.0}
なお、このAPIはゼロ除算のケースを考慮していないため、bに0を渡すと500エラーになります。これは、
また、OpenAPIスキーマの内容はhttp://で確認できます。以下のように、aとbは整数型を受け付けるように定義されています。
 
次に、テストコードを書きます。main.
import schemathesis
# (1)OpenAPI仕様書のURLを指定してスキーマを生成
schema = schemathesis.openapi.from_url("http://127.0.0.1:8000/openapi.json")
@schema.parametrize()  # (2)スキーマと戦略の定義に基づいてテストケースを生成
def test_api(case):
    """pytestでAPIのテストを実行する関数"""
    # (3)Schemathesisが生成したテストケースを使用してAPIを呼び出し、検証を行う
    case.call_and_validate()
SchemathesisがOpenAPIのドキュメントを読み取り(1))、仕様を元に生成したテストデータをテスト関数に渡して(2))(3))
test_関数の引数caseに入っているのは、SchemathesisのCaseというデータクラスです。ここには、Schemathesisが生成したテストデータが格納されています。
fastapi dev main.でアプリケーションを立ち上げた状態で、pytestを実行してみましょう。パラメータbに0を渡してゼロ除算になり、仕様に反するステータス500を返したため、テストが失敗しています。また、curlコマンドのスニペットが出力され、エラーになったテスト結果をシェルから再現できるようになっています。
(.venv) $ pytest test_main.py -v
============================= test session starts ==============================
(省略)
test_main.py::test_api[POST /div] FAILED                                 [100%]
=================================== FAILURES ===================================
_____________________________ test_api[POST /div] ______________________________
+ Exception Group Traceback (most recent call last):
(省略)
  | - Server error
  | 
  | - Undocumented HTTP status code
  | 
  |     Received: 500
  |     Documented: 200, 422
  | 
  | [500] Internal Server Error:
  | 
  |     `Internal Server Error`
  | 
  | Reproduce with: 
  | 
  |     curl -X POST -H 'Content-Type: application/json' -d '{"a": 0, "b": 0}' http://127.0.0.1:8000/div
  | 
  |  (2 sub-exceptions)
  +-+---------------- 1 ----------------
    | schemathesis.core.failures.ServerError: Server error
    +---------------- 2 ----------------
    | schemathesis.openapi.checks.UndefinedStatusCode: Undocumented HTTP status code
    | 
    | Received: 500
    | Documented: 200, 422
    +------------------------------------
=========================== short test summary info ============================
FAILED test_main.py::test_api[POST /div]
============================== 1 failed in 0.38s ===============================
アプリケーションの修正とテストの再実行
それでは、テストが失敗しないようにアプリケーションを修正します。入力値bに0を渡せないように、バリデーションルールを追加してみましょう。main.(1)、(2)の部分)。
import fastapi
from pydantic import BaseModel, Field, StrictInt  # (1)Fieldをインポート
app = fastapi.FastAPI()
class Values(BaseModel):
    a: StrictInt
    # (2)Fieldを使用してOpenAPIの拡張を追加
    b: StrictInt = Field(
        ...,
        description="0以外の整数",
        json_schema_extra={
            "not": {"const": 0}  # OpenAPI拡張:b != 0 の意味
        }
    )
@app.post("/div")
async def div(values: Values):
    """2つの整数を受け取り、その商を返すAPIエンドポイント"""
    return {"result": values.a / values.b}
OpenAPIスキーマでは、bが0以外の整数であることを保証する旨が追記されます。
 
修正後に再度pytestを実行すると、Schemathesisは修正後のOpenAPIスキーマを読み取り、bに0を渡すとバリデーションエラーとしてステータスコード422を返すことを期待するテストケースを作成します。アプリケーションは仕様どおりに実装されているので、テストが通ります。
(.venv) $ pytest test_main.py -v ============================= test session starts ============================== (省略) test_main.py::test_api[POST /div] PASSED [100%] ============================== 1 passed in 0.85s ===============================
テストデータのカスタマイズ方法
テストデータのカスタマイズ方法をいくつか紹介します。
settings()デコレータのmax_
以下の例では、生成するテストデータ数を500に変更しています。
import schemathesis
from schemathesis import settings  # (1) settingsをインポート
schema = schemathesis.openapi.from_url("http://127.0.0.1:8000/openapi.json")
# (2)max_examplesを500に設定
@settings(max_examples=500)
@schema.parametrize()
def test_api(case):
    """pytestでAPIのテストを実行する関数"""
    case.call_and_validate()
また、given()デコレータを使うことでテストの
以下の例では、aに100000以上の整数を渡すように指定し(1)、(2))、case引数に格納されたaのテストデータを上書きしています(3))。
aに100000以上の整数を渡すように指定import schemathesis
# (1) Hypothesis(Schemathesisが依存するテストフレームワーク)をインポート
from hypothesis import strategies as st
schema = schemathesis.openapi.from_url("http://127.0.0.1:8000/openapi.json")
# (2) aは100000以上の整数のみを使用する戦略に変更
@schema.given(a=st.integers(min_value=100000))
@schema.parametrize()
def test_api(case, a):
    """pytestでAPIのテストを実行する関数"""
    # (3)aに渡す値を上書き
    if "a" in case.path_parameters:
        case.path_parameters["a"] = a
    case.call_and_validate()
テストコードのもう1つの書き方
テストコードのもう1つの書き方についても紹介します。
これは、テスト対象がPythonで書かれたWSGIschemaオブジェクトの生成にfrom_from_関数の代わりにfrom_
例をお見せします。前述のtest_(1)、(2)の部分)。
import schemathesis
from main import app  # (1)FastAPIアプリケーションをインポート
# (2)from_asgi関数にOpenAPI仕様書のパスとアプリケーションを渡してスキーマを生成
schema = schemathesis.openapi.from_asgi("/openapi.json", app)
@schema.parametrize()
def test_api(case):
    """pytestでAPIのテストを実行する関数"""
    case.call_and_validate()
上記のテストコードでは、テスト対象のアプリケーションをASGIで直接呼び出すため、ネットワークを介したHTTPによるテストより高速です。また、あらかじめアプリケーションを立ち上げなくても、pytestのコマンド実行時にアプリケーションが立ち上がってくれます。なお、WSGIまたはASGIで通信する場合は、テスト失敗時にcurlコマンドのスニペットは表示されません。
コマンドラインインターフェースから利用する方法
続いて、Schemathesisをコマンドラインインターフェースから利用する方法を紹介します。
Schemathesisにはschemathesisというコマンドが提供されています
schemathesisコマンドでテストを実行するには、runというサブコマンドにOpenAPIスキーマのURLを渡します。
使用例をお見せします。前述のfastapi dev main.で立ち上げてから、schemathesis run http://を実行します。pytestから実行した際と同様に、OpenAPIスキーマを元にテストデータを作成し、テストを実行します。
以下は、テストに成功した場合のメッセージです
$ schemathesis run http://127.0.0.1:8000/openapi.json
Schemathesis dev
━━━━━━━━━━━━━━━━
 ✅  Loaded specification from http://127.0.0.1:8000/openapi.json (in 0.10s)
     Base URL:         http://127.0.0.1:8000/
     Specification:    Open API 3.1.0
     Operations:       1 selected / 1 total
 ✅  API capabilities:
     Supports NULL byte in headers:    ✘
 ⏭   Examples (in 0.11s)
     ⏭  1 skipped
 ✅  Coverage (in 0.40s)
     ✅ 1 passed
 ✅  Fuzzing (in 0.68s)
     ✅ 1 passed
===================================================================== SUMMARY ======================================================================
API Operations:
  Selected: 1/1
  Tested: 1
Test Phases:
  ✅ API probing
  ⏭  Examples
  ✅ Coverage
  ✅ Fuzzing
  ⏭  Stateful (not applicable)
Test cases:
  128 generated, 128 passed
Seed: 25425465587409846911775882801013537899
============================================================= No issues found in 1.20s =============================================================
テストに失敗した場合は以下のようなメッセージが出力されます
$ schemathesis run http://127.0.0.1:8000/openapi.json
Schemathesis dev
━━━━━━━━━━━━━━━━
 ✅  Loaded specification from http://127.0.0.1:8000/openapi.json (in 1.48s)
     Base URL:         http://127.0.0.1:8000/
     Specification:    Open API 3.1.0
     Operations:       1 selected / 1 total
 ✅  API capabilities:
     Supports NULL byte in headers:    ✘
 ⏭   Examples (in 0.11s)
     ⏭  1 skipped
 ❌  Coverage (in 0.36s)
     ❌ 1 failed
 ❌  Fuzzing (in 0.14s)
     ❌ 1 failed
===================================================================== FAILURES =====================================================================
____________________________________________________________________ POST /div _____________________________________________________________________
1. Test Case ID: cgqbU7
- Server error
- Undocumented HTTP status code
    Received: 500
    Documented: 200, 422
[500] Internal Server Error:
    `Internal Server Error`
Reproduce with:
    curl -X POST -H 'Content-Type: application/json' -d '{"a": 0, "b": 0}' http://127.0.0.1:8000/div
===================================================================== SUMMARY ======================================================================
API Operations:
  Selected: 1/1
  Tested: 1
Test Phases:
  ✅ API probing
  ⏭  Examples
  ❌ Coverage
  ❌ Fuzzing
  ⏭  Stateful (not applicable)
Failures:
  ❌ Server error: 1
  ❌ Undocumented HTTP status code: 1
Test cases:
  27 generated, 1 found 2 unique failures
Seed: 107349865083912536094560386602272888193
=============================================================== 2 failures in 0.63s ================================================================
なお、コマンドラインインターフェースではgiven()デコレータでテストの
schemathesis.tomlでテスト実行時の設定をカスタマイズする 
以下のいずれかの場所にschemathesis.
- カレントディレクトリ
- プロジェクトルート
- --config-file引数で渡したパス
以下がschemathesis.
# --headersに渡す値
# API_TOKENは環境変数として設定しておく
headers = { Authorization = "Bearer ${API_TOKEN}" }
# --continue-on-failureを有効にする
continue-on-failure = true
# --max-examplesに渡す値
generation.max-examples = 500
なお、schemathesis.
最後に
最後に、筆者がSchemathesisを使ってみた実感について書きます。
この記事を読んで、
なぜなら、Schemathesisがやってくれるテストの内容は予測できません。私はテストコードを
また、テストコードはテスト対象の仕様を説明するドキュメントとしての側面もあると思います。ところが、Schemathesisのテストコードは
- 人間が書いたテストコードでは気付けないエッジケースを見つけられる
- 仕様書と実装の乖離に気づくことができる
SchemathesisはPythonで書かれていないAPIに対しても使える、汎用性の高いツールです。OpenAPIやGraphQLを採用しているプロジェクトであれば導入しやすいのも嬉しいですね。興味を持った方はぜひ試してみてください!
