📌 この記事はこんな方におすすめ
- PythonとrequestsでPUT・PATCHリクエストのテストを書きたい方
- APIテストでデータ更新の検証方法を学びたいQAエンジニアの方
- PUTとPATCHの違いを理解してテストに活かしたい方
- pytestでCRUD操作のAPIテストを完成させたい方
✅ この記事を読むとわかること
- PUT(全体更新)とPATCH(部分更新)の違いと使い分け
- pytestとrequestsでPUT・PATCHのAPIテストを書く基本パターン
- 更新後のレスポンス検証(ステータス・ボディ・ヘッダー)
- raise_for_status()を使った実務レベルのテストコードの書き方
PythonでAPIテストを書くとき、PUTとPATCHはどちらも「データを更新する」リクエストです。pytestとrequestsを使って「全体更新が正しく動くか?」「部分更新で指定したフィールドだけ変わるか?」を自動で検証する方法を解説します。
この記事では、PUT・PATCHの違いから実務で使えるテストコードまでをまとめて紹介します。
00. APIテストにおけるPUTとPATCHの違い
PUTとPATCHはどちらも「更新」ですが、更新の範囲が異なります。
| PUT | PATCH | |
|---|---|---|
| 更新範囲 | リソース全体を置き換える | 指定したフィールドだけ更新する |
| 省略フィールド | 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テスト実装方法を解説しました。
| テストケース | 内容 |
|---|---|
| TC01 | PUTで全体更新時にステータスコード200が返ること |
| TC02 | raise_for_status()で4xx/5xxを検知 |
| TC03 | PUTで更新したデータがレスポンスに正しく反映されること |
| TC04 | Content-TypeがJSONであること |
| TC05 | PATCHで部分更新時にステータスコード200が返ること |
| TC06 | PATCHで指定したフィールドが更新されること |
| TC07 | PATCHで複数フィールドを同時に更新できること |
| TC08 | レスポンスのスキーマ(型)が正しいこと |
| TC09 | 存在しないリソースへのPUTで適切なステータスが返ること |
次の記事ではDELETEリクエストのAPIテスト(データ削除の検証)を解説します。

