はじめに
API開発において、仕様書との整合性確認やエッジケースのテストは重要ですが、手動で網羅的にテストするのは困難です。そして、Schemathesisは、OpenAPIやGraphQLの仕様書から自動的にテストケースを生成し、APIの堅牢性を検証できるPythonベースのテストツールです。
本記事では、インターン生の私が実際にSchemathesisを使用して理解した内容や、公式GitHubおよびドキュメントに記述されている内容をもとに、基本的な使い方から実践的な活用方法まで解説します。
Schemathesisとは
SchemathesisはProperty-Based Testing(プロパティベーステスト)の手法を用いて、API仕様書から自動的にテストケースを生成するツールです。
主な特徴
- 自動テスト生成: OpenAPI/GraphQL仕様からテストケースを自動生成
- プロパティベーステスト: 想定外のエッジケースを発見
- 包括的なカバレッジ: すべてのエンドポイント、パラメータ、メソッドを網羅的にテスト
- 高速実行: 並列実行による効率的なテスト
- CI/CD統合: GitHub ActionsやJenkinsなどと簡単に統合可能
- 詳細なレポート: 失敗したケースの再現方法を含むレポート生成
- リグレッションテスト: API変更による影響を迅速に検出
Schemathesisで検出できる問題の種類
1. HTTPステータスコードの不整合
OpenAPI仕様書に定義されていないHTTPステータスコードが返される場合に検出されます。
検出例:
❌ Undocumented HTTP status code
Received: 405
Documented: 200, 401, 403, 500
原因:
- 仕様書の記述漏れ
- 実装と仕様の不一致
- エラーハンドリングの不備
2. 必須ヘッダーの検証不足
必須として定義されているヘッダー(例: Authorization)が欠落している場合、適切なエラー(401)を返さない問題を検出します。
検出例:
❌ Missing header not rejected
Got 500 when missing required 'Authorization' header, expected 401
原因:
- 認証ミドルウェアの未初期化
- 例外ハンドリングの不備
- 依存関係の注入エラー
3. サポートされていないHTTPメソッド
OpenAPI仕様書で定義されていないHTTPメソッドに対して、405 Method Not Allowedを返さない問題を検出します。
検出例:
❌ Unsupported methods
Unsupported method PUT returned 401, expected 405 Method Not Allowed
原因:
- ルーティングレイヤーより先に認証が実行される
- FastAPI/Expressなどのフレームワークのデフォルト動作
- ミドルウェアの実行順序の問題
4. スキーマ検証の不一致
OpenAPI仕様で定義されたバリデーションルールと、実際のAPI実装のバリデーションが一致しない場合に検出されます。
検出例:
❌ Schema validation mismatch
51 operations mostly rejected generated data due to validation errors
原因:
- 仕様書の制約が実装より緩い
- 実装側に追加のバリデーションロジックが存在
- 暗黙的なビジネスルールが仕様書に記載されていない
5. サーバーエラー (5xx系)
予期しないサーバーエラーが発生する場合を検出します。
検出例:
❌ Server error
[500] Internal Server Error
原因:
- 未初期化のリソース(データベース接続など)
- Null参照エラー
- 例外の未処理
6. 無効なデータに対する受理
スキーマに準拠したデータがAPIによって拒否される、または準拠していないデータが受理される問題を検出します。
検出例:
❌ API rejected schema-compliant request
Valid data should have been accepted
Expected: 2xx, 401, 403, 404, 5xx
Schemathesisのテストフェーズ
1. Examples(サンプルテスト)
OpenAPI仕様書に記載されているexamplesを使用してテストを実行します。
components:
schemas:
User:
type: object
properties:
name:
type: string
example: "John Doe"
age:
type: integer
example: 30
2. Coverage(カバレッジテスト)
すべてのエンドポイント、メソッド、パラメータの組み合わせを網羅的にテストします。
- 各HTTPメソッドの検証
- パスパラメータの組み合わせ
- クエリパラメータの組み合わせ
- リクエストボディの検証
3. Fuzzing(ファジングテスト)
ランダムかつ予測不可能なデータを生成してAPIの堅牢性をテストします。
- 境界値のテスト
- 型違反のテスト
- エッジケースの検出
- 予期しない入力の処理
4. Stateful Testing(ステートフルテスト)
複数のAPIエンドポイントを連携させて、状態を持つシナリオをテストします。
例:
- ユーザー登録 (POST /users)
- ログイン (POST /auth/login)
- リソース作成 (POST /resources)
- リソース取得 (GET /resources/{id})
インストール
pipでインストールできます:
pip install schemathesis
基本的な使い方
1. CLIでの実行
最もシンプルな使い方は、コマンドラインから直接実行する方法です:
# ローカルのOpenAPI仕様ファイルをテスト
schemathesis run openapi.json --base-url=http://localhost:8000
# リモートのAPIをテスト
schemathesis run https://api.example.com/openapi.json
# 認証ヘッダーを追加
schemathesis run ./openapi.json \
--base-url=http://localhost:8000 \
--header "Authorization: Bearer YOUR_TOKEN"
# 特定のエンドポイントのみテスト
schemathesis run ./openapi.json \
--base-url=http://localhost:8000 \
--endpoint="/api/users"
# ワーカー数を指定して並列実行
schemathesis run ./openapi.json \
--base-url=http://localhost:8000 \
--workers=4
2. Pythonコードでの実行
pytest統合を使用して、より柔軟なテストが可能です:
import schemathesis
# OpenAPI仕様からスキーマをロード
schema = schemathesis.from_uri("https://api.example.com/openapi.json")
@schema.parametrize()
def test_api(case):
# テストケースを実行
response = case.call()
# ステータスコードと応答の妥当性を検証
case.validate_response(response)
3. カスタムチェックの追加
独自の検証ロジックを追加できます:
import schemathesis
schema = schemathesis.from_uri("https://api.example.com/openapi.json")
@schema.parametrize()
def test_api(case):
response = case.call()
# 標準検証
case.validate_response(response)
# カスタムチェック
if response.status_code == 200:
assert response.headers.get("Content-Type") == "application/json"
assert "data" in response.json()
高度な使い方
テスト対象のフィルタリング
特定のエンドポイントやメソッドに絞ってテストできます:
# 特定のエンドポイントのみテスト
schemathesis run openapi.json \
--base-url=http://localhost:8000 \
--endpoint="/users/*"
# 特定のメソッドのみテスト
schemathesis run openapi.json \
--base-url=http://localhost:8000 \
--method=POST
Pythonコードでは:
import schemathesis
schema = schemathesis.from_uri("https://api.example.com/openapi.json")
@schema.parametrize(endpoint="/api/users")
@schema.parametrize(method="POST")
def test_user_creation(case):
response = case.call()
case.validate_response(response)
認証の設定
APIキーやBearerトークンを使用した認証にも対応:
# APIキー認証
schemathesis run openapi.json \
--base-url=http://localhost:8000 \
--header="X-API-Key: your-api-key"
# Bearer認証
schemathesis run openapi.json \
--base-url=http://localhost:8000 \
--header="Authorization: Bearer your-token"
Pythonコードでは:
import schemathesis
schema = schemathesis.from_uri(
"https://api.example.com/openapi.json",
headers={"Authorization": "Bearer your-token"}
)
@schema.parametrize()
def test_api(case):
response = case.call()
case.validate_response(response)
テストケース数の調整
生成されるテストケース数を制御できます:
# 各エンドポイントに対して100ケース生成
schemathesis run openapi.json \
--base-url=http://localhost:8000 \
--hypothesis-max-examples=100
並列実行
複数のワーカーで並列実行することで、テスト時間を短縮:
schemathesis run openapi.json \
--base-url=http://localhost:8000 \
--workers=4
テスト結果の解釈
成功例
✅ Coverage (in 88.85s)
✅ 52 passed ❌ 11 failed
✅ Fuzzing (in 176.71s)
✅ 62 passed ❌ 1 failed
失敗の種類別サマリー
Failures:
❌ Server error: 1
❌ Missing header not rejected: 1
❌ Unsupported methods: 10
Warnings:
⚠️ Missing authentication: 61 operations returned only 401/403
⚠️ Schema validation mismatch: 51 operations rejected data
CI/CDへの統合
GitHub Actionsの例
name: API Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.9'
- name: Install dependencies
run: |
pip install schemathesis
- name: Start API server
run: |
docker-compose up -d
sleep 10
- name: Run Schemathesis tests
run: |
schemathesis run ./openapi.json \
--base-url=http://localhost:8000 \
--workers=4 \
--checks all
実践的なTips
1. 失敗したケースの再現
Schemathesisは失敗したテストケースを再現可能な形で出力します:
schemathesis run openapi.json \
--base-url=http://localhost:8000 \
--show-errors-tracebacks
2. 特定のチェックを無効化
必要に応じて特定のチェックをスキップできます:
schemathesis run openapi.json \
--base-url=http://localhost:8000 \
--exclude-checks=status_code_conformance
3. テストの除外設定
特定のエンドポイントやケースを除外したい場合:
import schemathesis
schema = schemathesis.from_uri("./openapi.json")
# 特定のエンドポイントを除外
@schema.parametrize(endpoint="^(?!/admin).*")
def test_api(case):
response = case.call()
case.validate_response(response)
トラブルシューティング
認証エラーが多発する場合
問題: すべてのテストで401エラーが返される
解決策:
- 有効な認証トークンを使用しているか確認
- トークンの有効期限を確認
-
--headerオプションで正しくヘッダーを設定
タイムアウトエラーが発生する場合
問題: テストが途中でタイムアウトする
解決策:
# タイムアウト時間を延長
schemathesis run ./openapi.json \
--base-url=http://localhost:8000 \
--request-timeout=30000
大量のエラーが発生する場合
問題: 数百件のエラーが報告される
解決策:
- まず仕様書と実装の基本的な部分を確認
- 段階的にテスト範囲を拡大
- 特定のエンドポイントから始める
# 1つのエンドポイントのみテスト
schemathesis run ./openapi.json \
--base-url=http://localhost:8000 \
--endpoint="/api/health"
Schemathesisの利点
1. 開発効率の向上
- 手動テストケース作成の削減
- リグレッションテストの自動化
- 仕様変更の影響を即座に検出
2. 品質の向上
- エッジケースの自動検出
- 仕様と実装の整合性確保
- セキュリティ脆弱性の早期発見
3. ドキュメント駆動開発
- OpenAPI仕様書が単一の信頼できる情報源に
- フロントエンドとバックエンドの契約が明確に
- チーム間のコミュニケーション向上
まとめ
Schemathesisを使用することで、APIテストの自動化と網羅性を大幅に向上させることができます。以下のような開発フローで活用できます:
- 設計フェーズ: OpenAPI仕様書を作成
- 開発フェーズ: Schemathesisでテストを実行しながら実装
- テストフェーズ: 包括的なテストで品質を確保
- 運用フェーズ: CI/CDパイプラインで継続的にテスト実行
プロパティベーステストの手法により、開発者が想定していないエッジケースを発見できるため、より堅牢なAPIを構築することができます。まずは小規模なプロジェクトで試してみて、徐々に本格的な導入を検討してみてはいかがでしょうか。