📌 이 글은 이런 분께 추천합니다
- Python과 requests로 PUT・PATCH 요청 테스트를 작성하고 싶은 분
- API 테스트에서 데이터 업데이트 검증 방법을 배우고 싶은 QA 엔지니어
- PUT과 PATCH의 차이를 이해하고 테스트에 활용하고 싶은 분
- pytest로 CRUD 조작의 API 테스트를 완성하고 싶은 분
✅ 이 글을 읽으면 알 수 있는 것
- PUT(전체 업데이트)과 PATCH(부분 업데이트)의 차이와 사용 구분
- pytest와 requests로 PUT・PATCH API 테스트를 작성하는 기본 패턴
- 업데이트 후 응답 검증(상태・바디・헤더)방법
- raise_for_status()를 사용한 실무 수준의 테스트 코드 작성법
Python과 pytest로 API 테스트를 작성할 때 PUT과 PATCH는 둘 다 「데이터를 업데이트하는」 요청입니다. pytest와 requests를 사용해서 「전체 업데이트가 올바르게 동작하는가?」「부분 업데이트에서 지정한 필드만 바뀌는가?」를 자동으로 검증하는 방법을 해설합니다.
이 글에서는 PUT・PATCH의 차이부터 실무에서 사용할 수 있는 테스트 코드까지를 정리해서 소개합니다.
00. API 테스트에서 PUT과 PATCH의 차이
PUT과 PATCH는 둘 다 「업데이트」이지만, 업데이트 범위가 다릅니다.
| PUT | PATCH | |
|---|---|---|
| 업데이트 범위 | 리소스 전체를 교체한다 | 지정한 필드만 업데이트한다 |
| 생략한 필드 | null이나 기본값이 된다 | 변경되지 않고 원래 값이 유지된다 |
| 멱등성 | 있음(몇 번 보내도 같은 결과) | 구현에 따라 다름 |
| 용도 | 프로필 전체 업데이트 등 | 이메일 주소만 변경 등 |
💡 포인트:실무에서는 PATCH가 더 많이 사용됩니다. 하나의 필드만 변경하고 싶을 때 전체 필드를 전송할 필요가 없기 때문입니다.
01. pytest + requests의 API 테스트 환경
아직 환경 구축이 안 된 분은 먼저 설치해주세요.
pip install requests pytest pytest-html이번 테스트 대상은 무료 목(mock) 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했을 때 적절한 상태가 반환되어야 한다"""
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가 같은 엔드포인트 URL을 사용하기 때문에 테스트 코드를 작성할 때 혼동해버렸습니다.
# ❌ 메서드를 착각해버린다
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"}
response1 = requests.put(url, json=updated_user) # 1회째
response2 = requests.put(url, json=updated_user) # 2회째(같은 요청)
# 같은 결과가 반환되는 것을 확인
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. PATCH 전에 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 테스트(데이터 삭제 검증)를 해설합니다.

