Playwright + Python で認証フローAPIテストを自動化する方法|トークン取得・CRUD・セキュリティ検証

テスト自動化

📌 この記事はこんな方におすすめ

  • PlaywrightのAPIRequestContextを使ったAPIテストを始めたい方
  • 認証フロー(トークン取得・付与・拒否)のテスト実装に興味がある方
  • pytestのfixtureを活用してテストデータを効率的に管理したい方
  • REST APIのCRUD操作(POST・PUT・DELETE)を自動テストしたい方

✅ この記事を読むとわかること

  • PlaywrightのAPIRequestContextでAPIテストを実装する方法
  • 認証トークンをfixtureで管理してテスト間で共有するパターン
  • GET/POST/PUT/DELETE の各HTTPメソッドのテスト実装
  • 認証なしアクセスを正しく拒否することの自動テスト

👤

この記事を書いた人:QAエンジニアとしてPlaywright・Python・pytestを使ったAPIテスト自動化を実務で担当。この記事で使用するコードはすべてGitHubで公開しています。GitHubでコードを見る →

APIテストというと「requestsライブラリ + pytest」の組み合わせが有名ですが、実はPlaywrightにもAPIRequestContextという強力なAPIテスト機能が搭載されています。

この記事では、ホテル予約APIの練習サイト Restful Booker を使って、認証フロー(ログイン→トークン取得→認証付きリクエスト)を6つのテストケースで自動化する実装例を解説します。


対象API・テスト構成

使用するAPI

項目 内容
対象サイト Restful Booker(ホテル予約API練習サイト)
BASE URL restful-booker.herokuapp.com
フレームワーク Playwright(Python)+ pytest
認証方式 トークン認証(Cookie: token=xxx)

6つのテストケース

TC HTTPメソッド 内容 期待ステータス
TC01 POST /auth 正常ログイン → トークン取得 200
TC02 POST /auth 異常ログイン(誤パスワード)→ エラー確認 200 + reason
TC03 POST /booking トークンを使って予約作成 200
TC04 PUT /booking/{id} トークンを使って予約更新 200
TC05 DELETE /booking/{id} トークンを使って予約削除 201
TC06 DELETE /booking/{id} トークンなしで削除 → 拒否確認 403

環境構築

必要なパッケージのインストール

# Playwright + pytestのインストール
pip install playwright pytest pytest-playwright

# Playwrightのブラウザをインストール(APIテストはブラウザ不要だが念のため)
playwright install
💡 ポイント: PlaywrightのAPIRequestContextはブラウザを起動しません。APIテストだけなら playwright install は省略できますが、将来E2Eテストと組み合わせる場合は入れておくと便利です。

pytest.ini:HTMLレポートを自動生成する

プロジェクトのルートに pytest.ini を置くことで、テスト実行のたびにHTMLレポートが自動生成されます。テスト結果をブラウザで確認できるようになり、エビデンスとしても活用できます。

▼ フォルダ構成

project/
├── pytest.ini          # ← ここに置く
├── test_auth_api.py
└── report.html         # ← 実行後に自動生成される

▼ pytest.ini の内容

[pytest]
addopts = --html=report.html --self-contained-html

▼ 各オプションの意味

オプション 意味
--html=report.html report.html という名前でHTMLレポートを生成する
--self-contained-html CSSや画像をHTMLに埋め込む。ファイル1つで完結するため共有しやすい

▼ HTMLレポートの実行方法

# pytest.iniがあれば追加オプション不要で自動的にHTMLレポートが生成される
pytest test_auth_api.py -v -s

# 実行後、同フォルダに report.html が生成される
# ブラウザで開くとテスト結果が一覧で確認できる
メリット: pytest.ini にオプションを書いておけば、毎回コマンドに --html=report.html を打つ必要がなくなります。チームで共有するときも設定が統一されます。
⚠️ 注意: pytest-html プラグインが必要です。未インストールの場合は先に追加してください。

pip install pytest-html

report.html がうまく表示されないときの対処法

report.html をダブルクリックで直接ブラウザで開くと、セキュリティ制限(CORS)によってスタイルが崩れたり内容が表示されないことがあります。そのような場合は、Pythonの内蔵サーバーを使ってローカルでホストする方法が有効です。

▼ 手順

STEP 操作内容
1 report.html があるフォルダでターミナルを開く
2 以下のコマンドを実行してローカルサーバーを起動する
3 ブラウザで localhost:8080/report.html を開く(URLバーに直接入力)
# STEP 2:ローカルサーバーを起動(report.htmlがあるフォルダで実行)
python -m http.server 8080

# 起動後、ターミナルに以下のようなメッセージが表示される
# Serving HTTP on 0.0.0.0 port 8080 (http://0.0.0.0:8080/) ...
# STEP 3:ブラウザでこのURLを開く
localhost:8080/report.html
確認できること: テストの PASS / FAIL 一覧・実行時間・エラーの詳細・print() で出力した内容(-s オプション使用時)
💡 サーバーを止めるには: ターミナルで Ctrl + C を押してください。python -m http.server はPythonに標準搭載されており、追加インストールは不要です。

PlaywrightのAPIテスト基本パターン

実際のコードを見る前に、PlaywrightのAPIテストで押さえておきたい4つの基本パターンを整理します。

① APIRequestContext:HTTPクライアントの作成

PlaywrightでAPIリクエストを送るには、まず APIRequestContext を作成します。base_url を指定することで、以降のリクエストでパスだけ書けばよくなります。

# base_urlを指定してAPIRequestContextを作成
request_context = playwright.request.new_context(
    base_url="https://restful-booker.herokuapp.com"
)

# 以降はパスだけでリクエストを送れる
response = request_context.get("/booking")

② トークン取得:認証エンドポイントへのPOST

認証APIにPOSTリクエストを送り、レスポンスのJSONからトークンを取り出す基本パターンです。

response = request_context.post(
    "/auth",
    data={"username": "admin", "password": "password123"}
)

# レスポンスJSONからトークンを取得
token = response.json()["token"]
print(token)  # → "abc123xyz..."

③ 認証ヘッダーの渡し方:Cookie方式 vs Bearer方式

取得したトークンをAPIに渡す方法は、APIの仕様によって異なります。今回使用するRestful BookerはCookie方式です。一般的なREST APIではBearer方式が多いため、両方を覚えておくと他のAPIにも応用できます。

方式 ヘッダーの書き方 主な用途
Cookie方式 ← 今回 "Cookie": f"token={token}" Restful Bookerなど一部のAPI
Bearer方式 "Authorization": f"Bearer {token}" JWT認証など一般的なREST API
# ✅ Cookie方式(今回のRestful Bookerで使用)
headers = {"Cookie": f"token={token}"}

# Bearer方式(一般的なREST APIで多い)
# headers = {"Authorization": f"Bearer {token}"}
💡 ポイント: テストするAPIのドキュメントで「どの方式でトークンを渡すか」を必ず確認しましょう。方式を間違えると 403 Forbidden が返ってきます。

④ APIテストの実行:リクエスト送信と検証

ヘッダーを組み立てたらリクエストを送信し、ステータスコードとレスポンスボディを検証します。

# GETリクエスト(認証ヘッダー付き)
response = request_context.get(
    "/booking/1",
    headers={"Cookie": f"token={token}"}
)

# ステータスコードの検証
assert response.status == 200, f"期待値200に対して{response.status}が返りました"

# レスポンスボディの検証
body = response.json()
assert body["firstname"] == "Taro", "firstnameが期待値と異なります"
💡 実務Tip: この4つのパターン(Context作成→トークン取得→ヘッダー設定→リクエスト&検証)が身につけば、どんなREST APIのテストにも応用できます。以降のコードはすべてこのパターンの組み合わせです。

fixture設計:トークンと予約IDを効率的に管理する

このテストではpytestのfixtureを3層構造で設計しています。認証トークンと予約IDをセッション全体で共有することで、テストごとにログインし直す無駄を省いています。

▼ fixture依存関係

api_request_context
セッション全体で1つ
auth_token
一度取得して全テストで共有
booking_id
TC03で作成→TC04/05で使用

fixture①:APIRequestContext

@pytest.fixture(scope="session")
def api_request_context(playwright: Playwright) -> APIRequestContext:
    context = playwright.request.new_context(base_url=BASE_URL)
    yield context
    context.dispose()  # テスト終了後に自動でリソースを解放
💡 scope="session" を指定することで、テストセッション全体で1つのコンテキストを再利用します。毎テストごとに接続を張り直さないため高速です。

fixture②:認証トークン

@pytest.fixture(scope="session")
def auth_token(api_request_context: APIRequestContext) -> str:
    response = api_request_context.post(
        "/auth",
        data={"username": USERNAME, "password": PASSWORD},
    )
    assert response.status == 200, "トークン取得に失敗しました"
    token = response.json().get("token")
    assert token, "レスポンスにtokenが含まれていません"
    return token
💡 認証トークンはセッションで1回だけ取得し、TC03〜TC06のすべてで使い回します。これがDRY原則の実践です。

fixture③:テスト用予約ID

@pytest.fixture(scope="session")
def booking_id(api_request_context: APIRequestContext, auth_token: str) -> int:
    response = api_request_context.post(
        "/booking",
        headers={"Content-Type": "application/json", "Accept": "application/json"},
        data="""{
            "firstname": "Taro",
            "lastname": "Yamada",
            "totalprice": 12000,
            "depositpaid": true,
            "bookingdates": {
                "checkin": "2025-01-01",
                "checkout": "2025-01-05"
            },
            "additionalneeds": "Breakfast"
        }""",
    )
    assert response.status == 200
    booking_id = response.json().get("bookingid")
    assert booking_id, "予約IDが取得できませんでした"
    return booking_id
💡 予約IDはセッション開始時に1回だけ作成します。TC03(作成確認)→TC04(更新)→TC05(削除)→TC06(削除済みで認証エラー確認)という流れで使い回します。

テストコード全文

TC01:正常ログイン → トークン取得

def test_tc01_login_success(api_request_context: APIRequestContext):
    """TC01: 正しい認証情報でトークンが取得できること"""
    response = api_request_context.post(
        "/auth",
        data={"username": USERNAME, "password": PASSWORD},
    )

    assert response.status == 200, f"ステータスコードが200ではありません: {response.status}"

    body = response.json()
    assert "token" in body, "レスポンスにtokenが含まれていません"
    assert len(body["token"]) > 0, "トークンが空です"

    print(f"\n✅ TC01 PASS | Token取得成功: {body['token']}")
検証ポイント: ステータス200 / レスポンスに token キーが存在 / トークンが空でないこと

TC02:異常ログイン(誤ったパスワード)

def test_tc02_login_failure(api_request_context: APIRequestContext):
    """TC02: 誤ったパスワードでトークンが発行されないこと"""
    response = api_request_context.post(
        "/auth",
        data={"username": USERNAME, "password": "wrongpassword"},
    )

    assert response.status == 200, f"ステータスコードが200ではありません: {response.status}"

    body = response.json()
    assert "token" not in body, "誤ったパスワードなのにトークンが返されました"
    assert body.get("reason") == "Bad credentials", (
        f"エラーメッセージが期待値と違います: {body.get('reason')}"
    )

    print(f"\n✅ TC02 PASS | 認証失敗を正しく検出: {body}")
⚠️ 注意ポイント: このAPIは認証失敗でもステータス200を返します(設計仕様)。代わりにレスポンスボディの reason: "Bad credentials" でエラーを判定します。実APIではステータス401が一般的ですが、APIによって仕様が異なります。

TC03:トークンを使って予約作成(POST)

def test_tc03_create_booking_with_token(
    api_request_context: APIRequestContext, auth_token: str, booking_id: int
):
    """TC03: 認証済みトークンで予約が作成できること"""
    assert booking_id > 0, "予約IDが正しくありません"

    print(f"\n✅ TC03 PASS | 予約作成成功 (BookingID: {booking_id})")
💡 設計ポイント: 予約作成の実処理は booking_id fixtureの中で行われます。TC03はfixtureが正常に実行できたことを確認するだけのシンプルなテストです。

TC04:トークンを使って予約更新(PUT)

def test_tc04_update_booking_with_token(
    api_request_context: APIRequestContext, auth_token: str, booking_id: int
):
    """TC04: 認証済みトークンで予約が更新できること"""
    response = api_request_context.put(
        f"/booking/{booking_id}",
        headers={
            "Content-Type": "application/json",
            "Accept": "application/json",
            "Cookie": f"token={auth_token}",  # トークンをCookieヘッダーで渡す
        },
        data="""{
            "firstname": "Hanako",
            "lastname": "Yamada",
            "totalprice": 15000,
            "depositpaid": false,
            "bookingdates": {
                "checkin": "2025-02-01",
                "checkout": "2025-02-07"
            },
            "additionalneeds": "Dinner"
        }""",
    )

    assert response.status == 200, f"更新に失敗しました: {response.status}"

    body = response.json()
    assert body.get("firstname") == "Hanako", "firstnameが更新されていません"
    assert body.get("totalprice") == 15000, "totalpriceが更新されていません"

    print(f"\n✅ TC04 PASS | 予約更新成功: {body}")
検証ポイント: ステータス200 / firstname が “Hanako” に更新 / totalprice が 15000 に更新

TC05:トークンを使って予約削除(DELETE)

def test_tc05_delete_booking_with_token(
    api_request_context: APIRequestContext, auth_token: str, booking_id: int
):
    """TC05: 認証済みトークンで予約が削除できること"""
    response = api_request_context.delete(
        f"/booking/{booking_id}",
        headers={"Cookie": f"token={auth_token}"},
    )

    assert response.status == 201, f"削除に失敗しました: {response.status}"

    print(f"\n✅ TC05 PASS | 予約削除成功 (BookingID: {booking_id})")
⚠️ 注意ポイント: Restful Bookerの DELETE は成功時に ステータス201 を返します。一般的なREST設計では200または204が多いですが、このAPIの仕様です。テストするAPIの仕様書を必ず確認しましょう。

TC06:トークンなしで保護エンドポイントへアクセス(拒否確認)

def test_tc06_delete_booking_without_token(
    api_request_context: APIRequestContext, booking_id: int
):
    """TC06: トークンなしで削除リクエストを送ると拒否されること"""
    # TC05で削除済みなので同IDで再度試みる(認証エラーを確認)
    response = api_request_context.delete(
        f"/booking/{booking_id}",
        headers={},  # Cookieなし(認証情報を渡さない)
    )

    assert response.status == 403, (
        f"トークンなしで削除できてしまいました: {response.status}"
    )

    print(f"\n✅ TC06 PASS | 未認証アクセスを正しく拒否: status={response.status}")
検証ポイント: 認証なしアクセスに対してステータス403が返ること。セキュリティテストの基本パターンです。

実行方法と結果サンプル

テストの実行

# 通常実行
pytest test_auth_api.py -v

# print出力も表示する場合
pytest test_auth_api.py -v -s

実行結果サンプル(ターミナル)

実際に実行した結果です。6テスト全てが PASSED、合計 5.07秒 で完了しました。

pytest実行結果ターミナル - 6 passed in 5.07s

▲ ターミナルでの実行結果 — TC01〜TC06 すべて PASSED

HTMLレポート(report.html)

pytest.ini の設定により自動生成された report.html をブラウザで開いた画面です。テスト名・実行時間・PASS/FAILが一覧で確認できます。

report.html - 6 Passed

▲ report.html の表示結果 — 0 Failed / 6 Passed / 合計 5秒


設計のポイント:なぜこの構造にしたのか

🔑
fixtureでトークンを共有

毎テストでログインし直すのではなく、セッションで1回だけ取得。DRY原則の実践。

🏗
fixture依存関係の活用

booking_id fixture が auth_token に依存する設計で、前提条件を自動保証。

🔒
セキュリティテストも含む

TC06で「拒否されること」を検証。正常系だけでなく異常系・セキュリティも自動化。

ブラウザ不要・高速

APIRequestContextはブラウザを起動しないため、6テストが3秒台で完了。

まとめ

この記事では、PlaywrightのAPIRequestContextとpytestを使って認証フローを6つのテストケースで自動化する方法を解説しました。

📋 この記事のまとめ

  • PlaywrightのAPIRequestContextはブラウザ不要で高速なAPIテストが書ける
  • 認証トークンは scope="session" のfixtureで管理することで全テストで共有できる
  • POST・PUT・DELETEの各HTTPメソッドの実装パターンを習得できた
  • 異常系(誤パスワード)・セキュリティ(トークンなし拒否)のテストも自動化できる
  • APIによってステータスコードの仕様が異なるため、必ずAPIドキュメントを確認する

PlaywrightはE2EテストだけでなくAPIテストにも対応しています。E2EとAPIテストを同じフレームワークで統一できるのも大きなメリットです。

タイトルとURLをコピーしました