PythonでAPIのPUT・PATCHテストを書く方法|pytest×requestsでデータ更新を検証

テスト自動化

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

  • PythonとrequestsでPUT・PATCHリクエストのテストを書きたい方
  • APIテストでデータ更新の検証方法を学びたいQAエンジニアの方
  • PUTとPATCHの違いを理解してテストに活かしたい方
  • pytestでCRUD操作のAPIテストを完成させたい方

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

  • PUT(全体更新)とPATCH(部分更新)の違いと使い分け
  • pytestとrequestsでPUT・PATCHのAPIテストを書く基本パターン
  • 更新後のレスポンス検証(ステータス・ボディ・ヘッダー)
  • raise_for_status()を使った実務レベルのテストコードの書き方

👨‍💻 筆者について

QAエンジニアとして実務でPython・pytest・requestsを使ったAPIテスト自動化を担当。本記事で使用するコードはすべてGitHubで公開しており、実際に動作確認済みのコードをそのまま解説しています。GitHubでコードを見る →

PythonでAPIテストを書くとき、PUTとPATCHはどちらも「データを更新する」リクエストです。pytestとrequestsを使って「全体更新が正しく動くか?」「部分更新で指定したフィールドだけ変わるか?」を自動で検証する方法を解説します。

この記事では、PUT・PATCHの違いから実務で使えるテストコードまでをまとめて紹介します。


00. APIテストにおけるPUTとPATCHの違い

PUTとPATCHはどちらも「更新」ですが、更新の範囲が異なります。

PUTPATCH
更新範囲リソース全体を置き換える指定したフィールドだけ更新する
省略フィールドnullやデフォルト値になる変更されず元の値が保持される
冪等性あり(何度送っても同じ結果)実装依存
用途プロフィール全体の更新などメールアドレスだけ変更など

💡 ポイント:実務ではPATCHの方が多く使われます。ユーザーが1つのフィールドだけ変更したいときに、全フィールドを送信する必要がないためです。


01. pytest + requestsのAPIテスト環境

まだ環境構築が済んでいない方は先にインストールしてください。

pip install requests pytest pytest-html

今回のテスト対象は無料のモックAPI JSONPlaceholder です。

# PUTリクエスト(全体更新)
PUT https://jsonplaceholder.typicode.com/users/1

# PATCHリクエスト(部分更新)
PATCH https://jsonplaceholder.typicode.com/users/1

💡 ポイント:JSONPlaceholderはモックAPIのため実際にはデータが更新されません。ただしレスポンスは本番APIと同じ形式で返ってくるため、テスト学習に最適です。


02. PythonでAPIのPUTテストを書く方法

基本的なPUTテスト(200確認)

PUTリクエストで全体更新し、200が返ることを確認します。

import requests

BASE_URL = "https://jsonplaceholder.typicode.com"

def test_put_user_status_code():
    """TC01: PUTで全体更新時にステータスコード200が返ること"""
    updated_user = {
        "id": 1,
        "name": "Updated Yoshitsugu",
        "username": "yoshitsugu728",
        "email": "updated@example.com",
        "phone": "090-9999-9999",
        "website": "qa-auto-lab.com"
    }
    response = requests.put(f"{BASE_URL}/users/1", json=updated_user)

    assert response.status_code == 200, \
        f"期待値: 200, 実際の値: {response.status_code}"

    print(f"\n✅ TC01 PASS | ステータスコード: {response.status_code}")

raise_for_status()で4xx/5xxを確実に検知する

def test_put_user_raise_for_status():
    """TC02: 4xx/5xxエラー時に例外が発生すること"""
    updated_user = {"id": 1, "name": "Updated User", "email": "updated@example.com"}
    response = requests.put(f"{BASE_URL}/users/1", json=updated_user)

    response.raise_for_status()

    assert response.status_code == 200
    print(f"\n✅ TC02 PASS | raise_for_status() 正常通過")

💡 ポイント:response.raise_for_status()実務で必須の1行です。PUT・PATCHテストでも必ず追加しておきましょう。

PUTのレスポンスボディ検証

更新したデータがレスポンスに正しく反映されているか確認します。

def test_put_user_response_body():
    """TC03: PUTで更新したデータがレスポンスに正しく反映されること"""
    updated_user = {
        "id": 1,
        "name": "Updated Yoshitsugu",
        "email": "updated@example.com"
    }
    response = requests.put(f"{BASE_URL}/users/1", json=updated_user)

    response.raise_for_status()
    body = response.json()

    assert body["name"] == updated_user["name"],   f"name が違います: {body['name']}"
    assert body["email"] == updated_user["email"], f"email が違います: {body['email']}"

    print(f"\n✅ TC03 PASS | 更新後の名前: {body['name']}")

ヘッダーの検証

def test_put_user_header():
    """TC04: Content-TypeがJSONであること"""
    updated_user = {"id": 1, "name": "Test", "email": "test@example.com"}
    response = requests.put(f"{BASE_URL}/users/1", json=updated_user)

    response.raise_for_status()

    content_type = response.headers.get("Content-Type", "")
    assert "application/json" in content_type, \
        f"Content-Typeが想定外: {content_type}"

    print(f"\n✅ TC04 PASS | Content-Type: {content_type}")

💡 ポイント:Content-Typeは application/json; charset=utf-8 の形式で返ってくるため、in の部分一致で検証します。


03. PythonでAPIのPATCHテストを書く方法

基本的なPATCHテスト(部分更新確認)

PATCHリクエストで一部のフィールドだけ更新し、200が返ることを確認します。

def test_patch_user_status_code():
    """TC05: PATCHで部分更新時にステータスコード200が返ること"""
    patch_data = {
        "email": "patched@example.com"  # emailだけ更新
    }
    response = requests.patch(f"{BASE_URL}/users/1", json=patch_data)

    assert response.status_code == 200, \
        f"期待値: 200, 実際の値: {response.status_code}"

    print(f"\n✅ TC05 PASS | ステータスコード: {response.status_code}")

PATCHのレスポンスボディ検証

指定したフィールドが更新されているか確認します。

def test_patch_user_response_body():
    """TC06: PATCHで指定したフィールドが更新されること"""
    patch_data = {"email": "patched@example.com"}
    response = requests.patch(f"{BASE_URL}/users/1", json=patch_data)

    response.raise_for_status()
    body = response.json()

    assert body["email"] == patch_data["email"], \
        f"emailが更新されていません: {body['email']}"

    print(f"\n✅ TC06 PASS | 更新後のemail: {body['email']}")

💡 ポイント:PATCHは指定したフィールドだけ変更するのが特徴です。他のフィールドが意図せず変更されていないかも合わせて確認するとより堅牢なテストになります。

PATCHで複数フィールドを更新

def test_patch_user_multiple_fields():
    """TC07: PATCHで複数フィールドを同時に更新できること"""
    patch_data = {
        "name": "Patched Name",
        "email": "patched@example.com"
    }
    response = requests.patch(f"{BASE_URL}/users/1", json=patch_data)

    response.raise_for_status()
    body = response.json()

    assert body["name"] == patch_data["name"],   f"name が違います: {body['name']}"
    assert body["email"] == patch_data["email"], f"email が違います: {body['email']}"

    print(f"\n✅ TC07 PASS | 更新完了: {body['name']} / {body['email']}")

04. APIテストのPUT・PATCHスキーマ検証

更新後のレスポンスが正しい型かどうかも確認します。

def test_put_user_schema():
    """TC08: PUTレスポンスのスキーマ(型)が正しいこと"""
    updated_user = {"id": 1, "name": "Updated User", "email": "updated@example.com"}
    response = requests.put(f"{BASE_URL}/users/1", json=updated_user)

    response.raise_for_status()
    body = response.json()

    assert isinstance(body["id"],    int), f"idはint型が期待値: {type(body['id'])}"
    assert isinstance(body["name"],  str), f"nameはstr型が期待値: {type(body['name'])}"
    assert isinstance(body["email"], str), f"emailはstr型が期待値: {type(body['email'])}"

    print(f"\n✅ TC08 PASS | スキーマ検証完了")

05. APIテストの異常系パターン(PUT・PATCH)

存在しないリソースへのPUT(404確認)

def test_put_nonexistent_user():
    """TC09: 存在しないリソースへのPUTで404が返ること"""
    updated_user = {"id": 99999, "name": "Ghost User", "email": "ghost@example.com"}
    response = requests.put(f"{BASE_URL}/users/99999", json=updated_user)

    # JSONPlaceholderはモックのため200を返すこともある
    # 実際のAPIでは404が期待される
    assert response.status_code in [200, 404], \
        f"期待外のステータスコード: {response.status_code}"

    print(f"\n✅ TC09 PASS | 存在しないリソースへのPUT: {response.status_code}")

⚠️ 注意:JSONPlaceholderはモックAPIのため、存在しないIDに対しても200を返すことがあります。実際のAPIでは404が返ることを検証してください。


06. PUT・PATCHテストのコード全文

"""
PUT / PATCH API Test
Target: JSONPlaceholder (https://jsonplaceholder.typicode.com)
Framework: Python + requests + pytest
"""
import requests

BASE_URL = "https://jsonplaceholder.typicode.com"


def test_put_user_status_code():
    """TC01: PUTで全体更新時にステータスコード200が返ること"""
    updated_user = {"id": 1, "name": "Updated Yoshitsugu", "email": "updated@example.com"}
    response = requests.put(f"{BASE_URL}/users/1", json=updated_user)
    assert response.status_code == 200, \
        f"期待値: 200, 実際の値: {response.status_code}"
    print(f"\n✅ TC01 PASS | status: {response.status_code}")


def test_put_user_raise_for_status():
    """TC02: 4xx/5xxエラー時に例外が発生すること"""
    updated_user = {"id": 1, "name": "Updated User", "email": "updated@example.com"}
    response = requests.put(f"{BASE_URL}/users/1", json=updated_user)
    response.raise_for_status()
    assert response.status_code == 200
    print(f"\n✅ TC02 PASS | raise_for_status() 正常通過")


def test_put_user_response_body():
    """TC03: PUTで更新したデータがレスポンスに正しく反映されること"""
    updated_user = {"id": 1, "name": "Updated Yoshitsugu", "email": "updated@example.com"}
    response = requests.put(f"{BASE_URL}/users/1", json=updated_user)
    response.raise_for_status()
    body = response.json()
    assert body["name"] == updated_user["name"]
    assert body["email"] == updated_user["email"]
    print(f"\n✅ TC03 PASS | name: {body['name']}")


def test_put_user_header():
    """TC04: Content-TypeがJSONであること"""
    updated_user = {"id": 1, "name": "Test", "email": "test@example.com"}
    response = requests.put(f"{BASE_URL}/users/1", json=updated_user)
    response.raise_for_status()
    content_type = response.headers.get("Content-Type", "")
    assert "application/json" in content_type
    print(f"\n✅ TC04 PASS | Content-Type: {content_type}")


def test_patch_user_status_code():
    """TC05: PATCHで部分更新時にステータスコード200が返ること"""
    patch_data = {"email": "patched@example.com"}
    response = requests.patch(f"{BASE_URL}/users/1", json=patch_data)
    assert response.status_code == 200, \
        f"期待値: 200, 実際の値: {response.status_code}"
    print(f"\n✅ TC05 PASS | status: {response.status_code}")


def test_patch_user_response_body():
    """TC06: PATCHで指定したフィールドが更新されること"""
    patch_data = {"email": "patched@example.com"}
    response = requests.patch(f"{BASE_URL}/users/1", json=patch_data)
    response.raise_for_status()
    body = response.json()
    assert body["email"] == patch_data["email"]
    print(f"\n✅ TC06 PASS | email: {body['email']}")


def test_patch_user_multiple_fields():
    """TC07: PATCHで複数フィールドを同時に更新できること"""
    patch_data = {"name": "Patched Name", "email": "patched@example.com"}
    response = requests.patch(f"{BASE_URL}/users/1", json=patch_data)
    response.raise_for_status()
    body = response.json()
    assert body["name"] == patch_data["name"]
    assert body["email"] == patch_data["email"]
    print(f"\n✅ TC07 PASS | {body['name']} / {body['email']}")


def test_put_user_schema():
    """TC08: PUTレスポンスのスキーマ(型)が正しいこと"""
    updated_user = {"id": 1, "name": "Updated User", "email": "updated@example.com"}
    response = requests.put(f"{BASE_URL}/users/1", json=updated_user)
    response.raise_for_status()
    body = response.json()
    assert isinstance(body["id"],    int)
    assert isinstance(body["name"],  str)
    assert isinstance(body["email"], str)
    print(f"\n✅ TC08 PASS | スキーマ検証完了")


def test_put_nonexistent_user():
    """TC09: 存在しないリソースへのPUTで適切なステータスが返ること"""
    updated_user = {"id": 99999, "name": "Ghost User", "email": "ghost@example.com"}
    response = requests.put(f"{BASE_URL}/users/99999", json=updated_user)
    assert response.status_code in [200, 404]
    print(f"\n✅ TC09 PASS | status: {response.status_code}")

実行コマンド

pytest test_put_patch_api.py -v -s

実行結果サンプル

test_put_patch_api.py::test_put_user_status_code      PASSED
test_put_patch_api.py::test_put_user_raise_for_status PASSED
test_put_patch_api.py::test_put_user_response_body    PASSED
test_put_patch_api.py::test_put_user_header           PASSED
test_put_patch_api.py::test_patch_user_status_code    PASSED
test_put_patch_api.py::test_patch_user_response_body  PASSED
test_put_patch_api.py::test_patch_user_multiple_fields PASSED
test_put_patch_api.py::test_put_user_schema           PASSED
test_put_patch_api.py::test_put_nonexistent_user      PASSED

9 passed in 4.23s ✅

07. ハマりポイント

実装中に実際につまずいた箇所をまとめました。同じところでハマる方の参考になれば嬉しいです。


① PUTで省略したフィールドがnullになる

PUTで一部のフィールドだけ送信したところ、送らなかったフィールドがnullや空になってしまいました。PUTはリソース全体を置き換えるため、全フィールドを送らないとデータが消えます。

# ❌ PUTで一部だけ送ると他のフィールドが消える可能性
response = requests.put(url, json={"name": "Updated"})
# → phone, website などが null になってしまう!

# ✅ PUTは全フィールドを含めて送る
updated_user = {
    "id": 1,
    "name": "Updated",
    "email": "original@example.com",  # 変更しないフィールドも含める
    "phone": "090-1234-5678",
    "website": "qa-auto-lab.com"
}
response = requests.put(url, json=updated_user)

💡 ポイント:一部のフィールドだけ変更したい場合はPUTではなくPATCHを使うのが正解です。


② PATCHで更新されないフィールドをチェックし忘れる

PATCHで1フィールドだけ更新したとき、他のフィールドが意図せず変更されていないかの確認を忘れていました。

# ❌ 更新したフィールドだけ確認している
patch_data = {"email": "new@example.com"}
response = requests.patch(url, json=patch_data)
body = response.json()
assert body["email"] == "new@example.com"  # これだけだと不十分

# ✅ 更新していないフィールドも変わっていないことを確認
assert body["email"] == "new@example.com"   # 更新されていること
assert body["name"] == "Leanne Graham"      # 変わっていないこと

💡 ポイント:PATCHテストでは「変えたフィールドが変わっている」だけでなく「変えていないフィールドが変わっていない」も確認すると完璧です。


③ PUTとPATCHのエンドポイントを混同する

PUTとPATCHで同じエンドポイントを使うため、テストコードを書くとき混同してしまいました。

# ❌ メソッドを間違えてしまう
response = requests.put(url, json=patch_data)    # PATCHのつもりでPUTを使用

# ✅ メソッドを明確に使い分ける
# 全体更新 → PUT
response = requests.put(f"{BASE_URL}/users/1", json=full_data)

# 部分更新 → PATCH
response = requests.patch(f"{BASE_URL}/users/1", json=partial_data)

⚠️ 注意:URLは同じでもHTTPメソッドが異なります。関数名にput/patchを明記して混同を防ぎましょう。


④ 更新後のIDが変わっていないかの確認を忘れる

PUTで更新後、IDが意図せず変わっていないかの確認を忘れていました。

# ✅ 更新後もIDが同じであることを確認
updated_user = {"id": 1, "name": "Updated", "email": "updated@example.com"}
response = requests.put(f"{BASE_URL}/users/1", json=updated_user)
body = response.json()

assert body["id"] == 1,                   "IDが変わってはいけない"
assert body["name"] == "Updated",         "nameが更新されていること"
assert body["email"] == "updated@example.com", "emailが更新されていること"

💡 ポイント:更新操作でIDが変わっていないことの確認は実務でよく見落とされるテストです。必ず追加しましょう。


⑤ モックAPIで冪等性が確認できない

PUTの特徴である「何度送っても同じ結果になる(冪等性)」をJSONPlaceholderで確認しようとしましたが、モックのため毎回同じレスポンスが返り確認できませんでした。

# 冪等性の確認(実際のAPIで試す場合)
updated_user = {"id": 1, "name": "Idempotent User", "email": "idem@example.com"}

# 1回目
response1 = requests.put(url, json=updated_user)
# 2回目(同じリクエスト)
response2 = requests.put(url, json=updated_user)

# 同じ結果が返ることを確認
assert response1.json() == response2.json(), "PUTは冪等であるべき"

⚠️ 注意:冪等性の確認は実際のデータが変わるAPIで試す必要があります。JSONPlaceholderではデータが保持されないため確認できません。


08. よくある質問(FAQ)

Q. PUTとPATCHはどちらを使えばいいですか?
A. 一部のフィールドだけ変更したい場合はPATCH、リソース全体を置き換えたい場合はPUTを使います。実務ではPATCHの方が多く使われます。変更しないフィールドまで毎回送信する必要がないためです。

Q. PUTのテストでステータスコードは200と204どちらが正しいですか?
A. APIの設計によります。更新後にリソースを返す場合は200、返さない場合は204を返すAPIもあります。テストを書く前にAPIドキュメントで確認してください。今回のJSONPlaceholderは200を返します。

Q. PATCHで更新後、他のフィールドが変わっていないか確認する方法は?
A. まずGETで更新前のデータを取得しておき、PATCH後に再度GETして比較するのが確実です。更新したフィールドだけが変わり、他は元の値のままであることを検証できます。

Q. 冪等性(idempotency)とは何ですか?
A. 同じリクエストを何度送っても結果が変わらない性質のことです。PUTは冪等性を持ちますが、POSTは毎回新しいリソースが作られるため冪等ではありません。APIの品質検証において重要な概念です。

Q. PUT・PATCHテストはGET・POSTテストの後に書くべきですか?
A. はい。CRUD操作の順番通り「GET→POST→PUT/PATCH→DELETE」の順で実装するとテストの流れが自然になります。更新テストは既存データへのアクセスが前提となるため、GETとPOSTが動作することを先に確認しましょう。


09. まとめ

PythonでAPIのPUT・PATCHテストを実装する場合、pytestとrequestsを使ってステータスコード・レスポンスボディ・スキーマを検証することが実務の基本です。PUTは全体更新・PATCHは部分更新という違いを意識したテスト設計が重要です。

この記事では、pytestとrequestsを使ったAPIのPUT・PATCHテスト実装方法を解説しました。

テストケース内容
TC01PUTで全体更新時にステータスコード200が返ること
TC02raise_for_status()で4xx/5xxを検知
TC03PUTで更新したデータがレスポンスに正しく反映されること
TC04Content-TypeがJSONであること
TC05PATCHで部分更新時にステータスコード200が返ること
TC06PATCHで指定したフィールドが更新されること
TC07PATCHで複数フィールドを同時に更新できること
TC08レスポンスのスキーマ(型)が正しいこと
TC09存在しないリソースへのPUTで適切なステータスが返ること

次の記事ではDELETEリクエストのAPIテスト(データ削除の検証)を解説します。

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