Python開発者がpytestを選ぶのは、そのシンプルさゆえです。テストはtest_で始まる単なる関数であり、アサーションは単純なassertステートメントであり、残りはランナーが処理します。これにrequestsライブラリを組み合わせれば、重厚な儀式なしに、APIテストを自動化するための完全なコードファーストのフレームワークが手に入ります。
このチュートリアルでは、実際のpytest APIテストスイートを構築する方法を紹介します。プロジェクトのセットアップ、最初のリクエストテストの作成、フィクスチャによるセットアップロジックの共有、parametrizeを使用した多数の入力に対する同じテストの実行、およびレスポンスステータス、ボディ、JSONスキーマに対するアサーションを行います。すべての例で現実的なパブリックスタイルのAPIを使用しているため、コードを直接応用できます。
プロジェクトのセットアップ
必要な2つのライブラリを仮想環境にインストールします。
python -m venv .venv
source .venv/bin/activate
pip install pytest requests jsonschema
クリーンなレイアウトは、スイートが成長しても保守性を保ちます。
api-tests/
conftest.py # 共有フィクスチャ
test_users.py # usersエンドポイントのテスト
test_orders.py # ordersエンドポイントのテスト
pytest.ini # 設定
Pytestはテストを自動的に発見します。ファイルはtest_で始まるか、_test.pyで終わる必要があり、関数はtest_で始まる必要があり、テストクラスはTestで始まり、__init__メソッドを持ってはなりません。これらのルールに従えば、手動で発見を設定する必要はありません。自動テストという分野が初めての方は、自動テストとは何かに関する私たちの入門記事が背景情報を提供します。
APIテストに特にpytestを使用する理由は何でしょうか? requestsライブラリがHTTPを処理し、pytestはその周りのすべてを処理します。つまり、テストの発見、読みやすい失敗出力を伴うアサーション、フィクスチャを介したセットアップとティアダウン、parametrizeを介したデータ駆動型実行、そしてレポート作成です。2つの小さく、よく文書化されたライブラリを組み合わせるだけで、チームがアプリケーション自体に既に使用している可能性のある言語で、完全なAPI自動化フレームワークを構築できます。この近接性が重要です。コードと同じリポジトリにテストが存在することで、正直さが保たれます。なぜなら、破壊的な変更とその失敗するテストが同じプルリクエスト内で現れるからです。
初めてのAPIテストの作成
pytest APIテストはリクエストを送信し、レスポンスをアサートします。以下は、ユーザーエンドポイントに対するテストです。
import requests
BASE_URL = "https://api.example.com/v1"
def test_get_user_returns_200():
response = requests.get(f"{BASE_URL}/users/42")
assert response.status_code == 200
def test_get_user_returns_expected_fields():
response = requests.get(f"{BASE_URL}/users/42")
body = response.json()
assert body["id"] == 42
assert "email" in body
assert body["status"] == "active"
pytest -vでスイートを実行します。失敗したassertごとに、実際の値を示す詳細なレポートが生成されます。これはpytestの最高の機能の1つです。特別なアサーションメソッドは必要ありません。フレームワークは単純なassertステートメントを書き換え、豊富な出力を提供します。レスポンスに対して行うべきより広範なチェックについては、APIアサーションに関するガイドをご覧ください。
フィクスチャによるセットアップの共有
すべてのテストでベースURLとHTTPセッションを繰り返すのは無駄です。これを解決するのがフィクスチャです。フィクスチャは@pytest.fixtureでデコレートされた関数で、テストがパラメータとして名前を指定することでリクエストできる値を生成します。
共有フィクスチャはconftest.pyに配置し、すべてのテストファイルがインポートせずに使用できるようにします。
# conftest.py
import pytest
import requests
BASE_URL = "https://api.example.com/v1"
@pytest.fixture(scope="session")
def api_session():
session = requests.Session()
session.headers.update({"Accept": "application/json"})
yield session
session.close()
@pytest.fixture
def auth_token(api_session):
response = api_session.post(
f"{BASE_URL}/auth/login",
json={"email": "qa@example.com", "password": "test-pass"},
)
return response.json()["token"]
scope="session"引数は、セッションがテストごとにではなく、実行全体で一度だけ作成されることを意味します。yieldキーワードはセットアップとティアダウンを分離します。yieldより前のコードが最初に実行され、yieldより後のコードはフィクスチャがスコープ外になったときに実行されます。テストは必要なものをリクエストするだけです。
def test_create_order(api_session, auth_token):
response = api_session.post(
f"{BASE_URL}/orders",
headers={"Authorization": f"Bearer {auth_token}"},
json={"product_id": 7, "quantity": 2},
)
assert response.status_code == 201
assert response.json()["status"] == "pending"
フィクスチャは、古いsetup_functionやteardown_functionスタイルに代わるpytestの現代的なものです。それらはきれいに構成でき、スコープをサポートし、依存関係を明示的にします。そのため、公式のpytestフィクスチャドキュメントでは、デフォルトのアプローチとして推奨されています。
多数の入力に対する単一テストの実行
APIエンドポイントは通常、有効な値、無効な値、エッジケースなど、多くの入力に対してチェックする必要があります。それぞれに個別の関数を作成するのは面倒です。@pytest.mark.parametrizeデコレータは、1つのテスト本体を複数の入力リストに対して実行します。
import pytest
import requests
BASE_URL = "https://api.example.com/v1"
@pytest.mark.parametrize("user_id,expected_status", [
(42, 200),
(99999, 404),
(0, 404),
(-1, 400),
])
def test_get_user_status_codes(api_session, user_id, expected_status):
response = api_session.get(f"{BASE_URL}/users/{user_id}")
assert response.status_code == expected_status
これにより、1つの関数から4つの別々のテストケースが生成されます。それぞれが独立して実行され、報告されるため、単一の不正な入力が他の問題を隠すことはありません。parametrizeは、データ駆動型テストに対するpytestの組み込みの回答です。入力セットが大きくなる場合は、代わりにファイルからロードします。CSVおよびJSONを使用したデータ駆動型APIテストに関する私たちのガイドがそのパターンを説明しています。各入力がどのステータスコードを返すべきか不明な場合は、REST APIが使用すべきHTTPステータスコードに関するリファレンスが役立つでしょう。
レスポンスボディとスキーマのアサーション
ステータスコードは必要ですが、それだけでは十分ではありません。不正な形式のボディを持つ200レスポンスもバグです。パースされたJSONに対して直接アサートします。
def test_order_response_shape(api_session, auth_token):
response = api_session.post(
f"{BASE_URL}/orders",
headers={"Authorization": f"Bearer {auth_token}"},
json={"product_id": 7, "quantity": 2},
)
body = response.json()
assert isinstance(body["id"], int)
assert body["quantity"] == 2
assert body["total"] > 0
assert response.elapsed.total_seconds() < 1.0
より強力な保証のためには、JSONスキーマに対してボディを検証します。これは、手書きのチェックでは見逃される可能性のある、フィールド名の変更や欠落といった構造的なずれを捕捉します。
from jsonschema import validate
order_schema = {
"type": "object",
"required": ["id", "product_id", "quantity", "status", "total"],
"properties": {
"id": {"type": "integer"},
"product_id": {"type": "integer"},
"quantity": {"type": "integer", "minimum": 1},
"status": {"type": "string"},
"total": {"type": "number"},
},
}
def test_order_matches_schema(api_session, auth_token):
response = api_session.post(
f"{BASE_URL}/orders",
headers={"Authorization": f"Bearer {auth_token}"},
json={"product_id": 7, "quantity": 2},
)
validate(instance=response.json(), schema=order_schema)
スキーマ検証は、1つのスキーマがレスポンス全体の形状をカバーするため、フィールドごとのアサーションよりも効率的です。jsonschemaライブラリが標準的な選択肢であり、その検証ドキュメントでサポートされているキーワードが説明されています。
CIでのスイートの実行
pytestスイートは自動的に実行されることで真価を発揮します。pytestは失敗時にゼロ以外の終了コードを返すため、CIサーバーがビルドを失敗させるのに必要なものです。インライン表示のためにJUnitレポートを出力します。
pytest -v --junitxml=results.xml
このコマンドをGitHub Actionsのステップやその他のパイプラインに組み込めば、APIテストがすべてのコミットのゲートとなります。CI/CDパイプラインでのAPIテストのチュートリアルでは、トークンのシークレット挿入や環境選択を含む完全なセットアップを紹介しています。
pytestスイートの信頼性を維持するには、2つのCI習慣があります。まず、テストファイルにシークレットや環境URLをハードコードしないでください。環境変数から読み込むことで、同じスイートをCIのステージング環境とローカルで編集なしに実行できます。
import os
BASE_URL = os.environ.get("API_BASE_URL", "https://staging.example.com/v1")
次に、フィードバックを迅速にするために、独立したテストを並行して実行します。pytest-xdistプラグインは、pytest -n autoを使用することでテストをCPUコアに分散させます。並行実行は、テストが状態を共有しない場合にのみ機能します。これがテストデータ層が重要であるもう一つの理由です。実行順序に依存するスイートは、並行実行された瞬間に予測不能な失敗をします。
pytestスイートの保守性を維持する
50個のテストスイートは簡単です。500個のスイートには規律が必要です。3つの実践方法により、pytest APIフレームワークは成長しても健全に保たれます。
関連するテストをモジュールにグループ化し、クラスはセットアップを共有する場合にのみ使用し、装飾のためには使用しません。明確な関数セットを持つtest_orders.pyファイルは、巨大なファイルよりも読みやすいです。pytest.iniに登録されたマークを使用してテストにタグを付け、サブセットを実行できるようにします。例えば、高速なゲートのための@pytest.mark.smoke、完全なスイープのための@pytest.mark.slowです。すべてのコミットでスモークテストを実行し、毎晩フルテストを実行します。
設定を一元化します。ベースURL、スキーマ、共有フィクスチャはconftest.pyまたは小さな設定モジュールに属し、ファイル間でコピー&ペーストしてはなりません。ステージングURLが変更された場合は、1行を編集するだけで済みます。自動テストスクリプトの書き方に関する私たちのガイドで説明されているように、どのフレームワークにも当てはまるモジュール化の規律がここでも適用されます。2回書くものはすべてフィクスチャまたはヘルパーとして抽出してください。
プラットフォームを利用するべき時
pytestフレームワークは、チームがPythonを記述し、アプリケーションコードの隣にテストを配置したい場合に優れています。しかし、QAや製品担当者が貢献する必要がある場合や、接着コードを維持することなく、テストの設計、モック、実行を1か所で行いたい場合には、あまり便利ではありません。
Apidogはそのギャップを埋めます。ビジュアルなテスト構築、OpenAPI仕様に対するスキーマ検証、CSVおよびJSONからのデータ駆動型実行、CI用のCLIランナーをすべて、フィクスチャやアサーションコードを手書きすることなく提供します。多くのチームは両方を使用しています。論理的に複雑なシナリオにはpytestを、広範なカバレッジや、pytestスイートがテストするAPIの設計とモックにはApidogを使用します。Apidogをダウンロードして、午後のうちに実際のAPIエンドポイントで2つのアプローチを比較できます。
よくある質問
APIテストにPythonの組み込みunittestではなくpytestを使用する理由は何ですか?
pytestはボイラープレートが少なくて済みます。テストは単なる関数であり、アサーションは豊富な失敗出力を伴う単純なassertステートメントであり、フィクスチャはunittestのクラスベースのメソッドよりも柔軟にセットアップを処理します。pytestには大規模なプラグインエコシステムと、データ駆動型テストのための組み込みのparametrizeもあります。既存のunittestスタイルのテストも実行できるため、移行のリスクは低いです。
フィクスチャとparametrizeの違いは何ですか?
フィクスチャは、HTTPセッションや認証トークンなど、再利用可能なリソースを、それを要求する任意のテストに提供します。parametrizeは、異なる入力値に対して同じテスト本体を複数回実行します。フィクスチャはセットアップを共有し、parametrizeはケースを増やします。これらはうまく組み合わせることができ、パラメーター化されたテストでもフィクスチャに依存できます。
pytest APIテストでレスポンス時間をアサートすべきですか?
response.elapsed.total_seconds()を使用すれば可能です。緩やかな上限を設定することで、大きな回帰を捉えることができます。しかし、pytestは機能テストツールであり、負荷テスターではありません。実際のパフォーマンス作業には、専用のツールを使用してください。通常のネットワーク変動が不安定な失敗を引き起こさないよう、タイミングアサーションは寛大に設定してください。
pytestでAPIテストを独立させるにはどうすればよいですか?
リソースを作成およびクリーンアップするフィクスチャを介して各テストに独自のデータを与え、テスト実行順序に依存しないようにします。pytestはデフォルトでファイル順にテストを実行しますが、適切に設計されたスイートはその順序に依存しません。独立したテストは並行して実行でき、個別に失敗するため、デバッグがはるかに容易になります。
pytestはOpenAPI仕様に対してレスポンスを検証できますか?
pytest自体はできませんが、jsonschemaライブラリを使用してJSONスキーマに対して検証できます。また、OpenAPIドキュメントに対してレスポンスをチェックするプラグインも存在します。スキーマ検証がワークフローの中心である場合、OpenAPI仕様に対して自動的に検証するApidogのようなプラットフォームを使用すると、プラグインのセットアップの手間を省くことができます。
