2025年7月: Web APIのテストデータを自動生成してくれるツール「Schemathesis」の紹介¶
筒井@ryu22eです。 今月の『Python Monthly Topics』は、Web APIのテストデータを自動生成してくれるツール「[Schemathesis(スキーマセシス)(https://schemathesis.readthedocs.io/en/stable/)」を紹介します。
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系とは仕様が大幅に変わりました。詳細な変更点については以下のドキュメントを参照してください。
schemathesis/MIGRATION.md at v4.0.0 · schemathesis/schemathesis
本記事ではバージョン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.pyを作成します。整数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.py
でアプリケーションを立ち上げてから、以下のコマンドで動作確認してみます。
$ 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://127.0.0.1:8000/docs
で確認できます。
以下のように、a
とb
は整数型を受け付けるように定義されています。

OpenAPIスキーマの内容¶
次に、テストコードを書きます。main.pyと同じディレクトリに以下の内容のtest_main.pyを作成します。
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_api()
関数の引数case
に入っているのは、SchemathesisのCaseというデータクラスです。
ここには、Schemathesisが生成したテストデータが格納されています。
fastapi dev main.py
でアプリケーションを立ち上げた状態で、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.pyを以下のように変更してください(コメント(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以外の整数であることを保証する旨が追記されます。

変更後のOpenAPIスキーマの内容¶
修正後に再度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_examples引数に整数値を渡すことで、 生成するテストデータ数を調整することがきます。 デフォルトのテストデータ数は100です。
以下の例では、生成するテストデータ数を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()
【コラム】 Hypothesisとは何か¶
「テストデータのカスタマイズ方法」で紹介したサンプルコードの中に、from hypothesis import strategies as st
というコードが登場しました。
このコードの意味について説明します。
hypothesis
とは、Schemathesisが内部で利用しているテストフレームワークHypothesis(ハイポセシス)のモジュールです。
Hypothesisは「プロパティベーステスト」というテスト手法を実践するためのテストフレームワークです。
Schemathesisでは、内部でHypothesisを呼んで入力値を生成し、APIのテストを行っています。
本記事では「プロパティベーステスト」の説明は割愛しますが、以下の書籍が参考になります。
実践プロパティベーステスト ― PropErとErlang/Elixirではじめよう – 技術書出版と販売のラムダノート
テストコードのもう一つの書き方¶
テストコードのもう1つの書き方についても紹介します。
これは、テスト対象がPythonで書かれたWSGI(またはASGI)アプリケーションである場合に使える方法です。
前述の例ではpytestからhttpでアプリケーションと通信する前提で、schema
オブジェクトの生成にfrom_url()関数を使っていました。
from_url()
関数の代わりにfrom_wsgi()関数、
またはfrom_asgi()関数を使うと、WSGIまたはASGIで通信するテストになります。
例をお見せします。
前述のtest_main.pyを以下のように書き換えます(コメント(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というコマンドが提供されています(stというエイリアスコマンドもあります)。このschemathesisコマンドにOpenAPIスキーマのURLを渡すことで、コマンド経由でテストを実行することができます。
schemathesisコマンドでテストを実行するには、runというサブコマンドにOpenAPIスキーマのURLを渡します。
使用例をお見せします。
前述の「pytestと組み合わせた方法」で作ったアプリケーションをfastapi dev main.py
で立ち上げてから、
schemathesis run http://127.0.0.1:8000/openapi.json
を実行します。
pytestから実行した際と同様に、OpenAPIスキーマを元にテストデータを作成し、テストを実行します。
以下は、テストに成功した場合のメッセージです(「アプリケーションの修正とテストの再実行」の修正版のmain.pyを使っている前提です)。
$ 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 =============================================================
テストに失敗した場合は以下のようなメッセージが出力されます(「アプリケーションの作成とテストの実行」で作った修正前のmain.pyを使っている前提です)。 「アプリケーションの作成とテストの実行」でpytestを実行した際と同様、curlコマンドのスニペットが出力されます。
$ 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()
デコレータでテストの「戦略」を定義する機能がありません。
「戦略」の定義が必要な場合はpytestを使ってください。
schemathesis.tomlでテスト実行時の設定をカスタマイズする¶
以下のいずれかの場所にschemathesis.tomlファイルを置くことで、pytestまたはschemathesisコマンド実行時に渡すオプションの値を省略できます。
カレントディレクトリ
プロジェクトルート
--config-file引数で渡したパス
以下がschemathesis.tomlの記述例です。
# --headersに渡す値
# API_TOKENは環境変数として設定しておく
headers = { Authorization = "Bearer ${API_TOKEN}" }
# --continue-on-failureを有効にする
continue-on-failure = true
# --max-examplesに渡す値
generation.max-examples = 500
なお、schemathesis.tomlで利用可能なオプションの内容については、以下の公式ドキュメントを参照してください。
最後に¶
最後に、筆者がSchemathesisを使ってみた実感について書きます。
この記事を読んで、「このツールを使えば人間が書くテストコードは省略できるのでは?」と思われる方がいるかもしれませんが、 筆者が使ってみた実感としては、人間が書くテストコードは減らさないほうがいいという感想です。 なぜなら、Schemathesisがやってくれるテストの内容は予測できません。 私はテストコードを「自分が意図したとおりに動くか検証する」という意味合いで捉えているので、 何をテストするのかコントロールできないSchemathesisは、テストコードを完全に代替するものではないような気がしました。
また、テストコードはテスト対象の仕様を説明するドキュメントとしての側面もあると思います。 ところが、Schemathesisのテストコードは「pytestと組み合わせる方法」のtest_main.pyのような内容で、このコードだけでは仕様を読み取れません。 人間が書くテストコードが減ってしまうと、プログラマーが読める情報が減ってしまうというデメリットもあると感じました。 Schemathesisを導入する際は、従来通りのテストコードは書いた上で、冒頭で説明した以下の利点を活かすツールと考えたほうがいいでしょう。
人間が書いたテストコードでは気付けないエッジケースを見つけられる
仕様書と実装の乖離に気づくことができる
SchemathesisはPythonで書かれていないAPIに対しても使える、汎用性の高いツールです。 OpenAPIやGraphQLを採用しているプロジェクトであれば導入しやすいのも嬉しいですね。 興味を持った方はぜひ試してみてください!