📌 이 글은 이런 분께 추천합니다
- Python과 requests로 POST 요청 테스트를 작성하고 싶은 분
- API 테스트에서 데이터 생성 검증 방법을 배우고 싶은 QA 엔지니어
- 정상계・유효성 검사・이상계 POST 테스트를 구현하고 싶은 분
- pytest로 API 테스트를 자동화하여 포트폴리오에 활용하고 싶은 분
✅ 이 글을 읽으면 알 수 있는 것
- pytest와 requests로 POST 요청 API 테스트를 작성하는 기본 패턴
- 데이터 생성 후 응답 검증(상태・바디・헤더)방법
- 유효성 검사 오류의 이상계 테스트 구현 방법
- raise_for_status()를 사용한 실무 수준의 테스트 코드 작성법
Python과 pytest로 API 테스트를 작성할 때 GET 다음으로 구현하는 것이 POST 요청 테스트입니다. pytest와 requests를 사용해서 「데이터가 올바르게 생성되는가?」「유효성 검사 오류가 올바르게 반환되는가?」를 자동으로 검증하는 방법을 해설합니다.
이 글에서는 POST 요청의 기본부터 이상계・유효성 검사 검증까지를 실무에서 사용할 수 있는 코드와 함께 소개합니다.
00. API 테스트와 POST 요청의 기본
POST 요청은 「데이터를 새로 생성하는」 조작입니다. GET과 달리 요청 바디에 데이터를 포함하여 서버에 전송합니다.
POST 테스트에서는 주로 다음을 검증합니다.
| 확인 항목 | 내용 | 예 |
|---|---|---|
| 상태 코드 | 생성 성공 시 201이 반환되는가 | 201 Created |
| 응답 바디 | 전송한 데이터가 올바르게 반환되는가 | name과 email 값 |
| ID 부여 | 새로 생성된 리소스에 ID가 부여되어 있는가 | “id”: 11 |
| 유효성 검사 | 부정한 데이터를 보냈을 때 적절한 오류가 반환되는가 | 400 Bad Request |
💡 포인트:POST 테스트는 GET보다 검증 항목이 많습니다. 정상계뿐만 아니라 유효성 검사 오류의 이상계도 반드시 테스트함으로써 API 품질을 담보할 수 있습니다.
01. pytest + requests의 API 테스트 환경
아직 환경 구축이 안 된 분은 먼저 설치해주세요.
pip install requests pytest pytest-html이번 테스트 대상은 무료 목(mock) API JSONPlaceholder 입니다.
POST https://jsonplaceholder.typicode.com/users전송하는 요청 바디 샘플입니다.
new_user = {
"name": "Yoshitsugu Tester",
"username": "yoshitsugu728",
"email": "yoshitsugu@example.com",
"phone": "090-1234-5678",
"website": "qa-auto-lab.com"
}💡 포인트:JSONPlaceholder는 목 API이므로 실제로는 데이터가 저장되지 않습니다. 하지만 응답은 실제 API와 동일한 형식으로 반환되므로 테스트 학습에 최적입니다.
02. Python으로 API POST 테스트를 작성하는 방법
기본적인 POST 테스트(201 확인)
POST 요청을 보내고 201 Created가 반환되는 것을 확인합니다.
import requests
BASE_URL = "https://jsonplaceholder.typicode.com"
def test_post_user_status_code():
"""TC01: 데이터 생성 시 상태 코드 201이 반환되어야 한다"""
new_user = {
"name": "Yoshitsugu Tester",
"username": "yoshitsugu728",
"email": "yoshitsugu@example.com"
}
response = requests.post(f"{BASE_URL}/users", json=new_user)
assert response.status_code == 201, \
f"기댓값: 201, 실제 값: {response.status_code}"
print(f"\n✅ TC01 PASS | 상태 코드: {response.status_code}")raise_for_status()로 4xx/5xx를 확실히 검지
def test_post_user_raise_for_status():
"""TC02: 4xx/5xx 오류 시 예외가 발생해야 한다"""
new_user = {"name": "Test User", "email": "test@example.com"}
response = requests.post(f"{BASE_URL}/users", json=new_user)
# 4xx/5xx 때 자동으로 HTTPError 발생
response.raise_for_status()
assert response.status_code == 201
print(f"\n✅ TC02 PASS | raise_for_status() 정상 통과")💡 포인트:response.raise_for_status() 는 실무에서 필수 1행입니다. POST 테스트에서도 반드시 추가해두세요.
헤더 검증
def test_post_user_header():
"""TC03: Content-Type이 JSON이어야 한다"""
new_user = {"name": "Test User", "email": "test@example.com"}
response = requests.post(f"{BASE_URL}/users", json=new_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✅ TC03 PASS | Content-Type: {content_type}")💡 포인트:Content-Type은 application/json; charset=utf-8 형태로 반환되므로 == 완전 일치가 아닌 in 부분 일치로 검증합니다.
응답 바디 검증
전송한 데이터가 응답에 올바르게 포함되어 있는지 확인합니다.
def test_post_user_response_body():
"""TC04: 전송한 데이터가 응답에 올바르게 포함되어야 한다"""
new_user = {
"name": "Yoshitsugu Tester",
"username": "yoshitsugu728",
"email": "yoshitsugu@example.com"
}
response = requests.post(f"{BASE_URL}/users", json=new_user)
response.raise_for_status()
body = response.json()
assert body["name"] == new_user["name"], f"name 불일치: {body['name']}"
assert body["email"] == new_user["email"], f"email 불일치: {body['email']}"
print(f"\n✅ TC04 PASS | 생성된 사용자: {body['name']}")ID가 부여되었는지 확인
새로 생성된 리소스에 서버로부터 ID가 부여되었는지 확인합니다.
def test_post_user_id_assigned():
"""TC05: 생성된 리소스에 ID가 부여되어야 한다"""
new_user = {"name": "Test User", "email": "test@example.com"}
response = requests.post(f"{BASE_URL}/users", json=new_user)
response.raise_for_status()
body = response.json()
assert "id" in body, "응답에 id가 포함되어 있지 않습니다"
assert isinstance(body["id"], int), f"id는 int형이 기댓값: {type(body['id'])}"
assert body["id"] > 0, f"id는 양의 정수가 기댓값: {body['id']}"
print(f"\n✅ TC05 PASS | 부여된 ID: {body['id']}")💡 포인트:ID 부여 확인은 실무에서 자주 간과되는 테스트입니다. 서버가 ID를 자동 채번하고 있는지, 타입은 올바른지 함께 확인하세요.
03. API 테스트 POST 유효성 검사 검증
실무에서는 정상계뿐만 아니라 부정한 데이터를 보냈을 때 적절한 오류가 반환되는지도 반드시 검증합니다.
필수 필드 없이 전송(유효성 검사 오류 확인)
def test_post_user_missing_required_fields():
"""TC06: 필수 필드 없이 전송했을 때 적절한 오류가 반환되어야 한다"""
incomplete_user = {
"username": "yoshitsugu728"
# name과 email을 의도적으로 생략
}
response = requests.post(f"{BASE_URL}/users", json=incomplete_user)
# JSONPlaceholder는 목이므로 201을 반환
# 실제 API에서는 400 Bad Request가 기대됨
assert response.status_code in [201, 400], \
f"기댓값: 201 또는 400, 실제 값: {response.status_code}"
print(f"\n✅ TC06 PASS | 필수 필드 없을 때 상태: {response.status_code}")빈 문자열을 전송
def test_post_user_empty_string():
"""TC07: 빈 문자열을 전송했을 때 적절한 오류가 반환되어야 한다"""
invalid_user = {
"name": "", # 빈 문자열
"email": "" # 빈 문자열
}
response = requests.post(f"{BASE_URL}/users", json=invalid_user)
assert response.status_code in [201, 400, 422], \
f"예상 밖의 상태 코드: {response.status_code}"
print(f"\n✅ TC07 PASS | 빈 문자열 전송 시 상태: {response.status_code}")잘못된 이메일 형식을 전송
def test_post_user_invalid_email():
"""TC08: 잘못된 이메일 형식을 전송했을 때 적절한 오류가 반환되어야 한다"""
invalid_user = {
"name": "Test User",
"email": "not-an-email" # 이메일 형식이 아님
}
response = requests.post(f"{BASE_URL}/users", json=invalid_user)
assert response.status_code in [201, 400, 422], \
f"예상 밖의 상태 코드: {response.status_code}"
print(f"\n✅ TC08 PASS | 잘못된 이메일 전송 시 상태: {response.status_code}")⚠️ 주의:JSONPlaceholder는 목 API이므로 부정한 데이터를 보내도 201이 반환됩니다. 유효성 검사를 정확하게 테스트하려면 실제 유효성 검사 기능을 가진 API가 필요합니다. Restful Booker나 reqres.in 등을 사용하거나 직접 만든 API로 검증하세요.
04. API 테스트 POST 스키마 검증
생성된 리소스의 응답이 올바른 타입인지도 확인합니다.
def test_post_user_schema():
"""TC09: 응답 스키마(타입)가 올바르다"""
new_user = {
"name": "Yoshitsugu Tester",
"email": "yoshitsugu@example.com"
}
response = requests.post(f"{BASE_URL}/users", json=new_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'])}"
print(f"\n✅ TC09 PASS | 스키마 검증 완료")05. POST 테스트 전체 코드
"""
POST API Test
Target: JSONPlaceholder (https://jsonplaceholder.typicode.com)
Framework: Python + requests + pytest
"""
import requests
BASE_URL = "https://jsonplaceholder.typicode.com"
def test_post_user_status_code():
"""TC01: 데이터 생성 시 상태 코드 201이 반환되어야 한다"""
new_user = {"name": "Yoshitsugu Tester", "email": "yoshitsugu@example.com"}
response = requests.post(f"{BASE_URL}/users", json=new_user)
assert response.status_code == 201, \
f"기댓값: 201, 실제 값: {response.status_code}"
print(f"\n✅ TC01 PASS | status: {response.status_code}")
def test_post_user_raise_for_status():
"""TC02: 4xx/5xx 오류 시 예외가 발생해야 한다"""
new_user = {"name": "Test User", "email": "test@example.com"}
response = requests.post(f"{BASE_URL}/users", json=new_user)
response.raise_for_status()
assert response.status_code == 201
print(f"\n✅ TC02 PASS | raise_for_status() 정상 통과")
def test_post_user_header():
"""TC03: Content-Type이 JSON이어야 한다"""
new_user = {"name": "Test User", "email": "test@example.com"}
response = requests.post(f"{BASE_URL}/users", json=new_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✅ TC03 PASS | Content-Type: {content_type}")
def test_post_user_response_body():
"""TC04: 전송한 데이터가 응답에 올바르게 포함되어야 한다"""
new_user = {"name": "Yoshitsugu Tester", "email": "yoshitsugu@example.com"}
response = requests.post(f"{BASE_URL}/users", json=new_user)
response.raise_for_status()
body = response.json()
assert body["name"] == new_user["name"]
assert body["email"] == new_user["email"]
print(f"\n✅ TC04 PASS | name: {body['name']}")
def test_post_user_id_assigned():
"""TC05: 생성된 리소스에 ID가 부여되어야 한다"""
new_user = {"name": "Test User", "email": "test@example.com"}
response = requests.post(f"{BASE_URL}/users", json=new_user)
response.raise_for_status()
body = response.json()
assert "id" in body
assert isinstance(body["id"], int)
assert body["id"] > 0
print(f"\n✅ TC05 PASS | 부여된 ID: {body['id']}")
def test_post_user_missing_required_fields():
"""TC06: 필수 필드 없이 전송했을 때 적절한 오류가 반환되어야 한다"""
incomplete_user = {"username": "yoshitsugu728"}
response = requests.post(f"{BASE_URL}/users", json=incomplete_user)
assert response.status_code in [201, 400]
print(f"\n✅ TC06 PASS | status: {response.status_code}")
def test_post_user_empty_string():
"""TC07: 빈 문자열을 전송했을 때 적절한 오류가 반환되어야 한다"""
invalid_user = {"name": "", "email": ""}
response = requests.post(f"{BASE_URL}/users", json=invalid_user)
assert response.status_code in [201, 400, 422]
print(f"\n✅ TC07 PASS | status: {response.status_code}")
def test_post_user_invalid_email():
"""TC08: 잘못된 이메일 형식을 전송했을 때 적절한 오류가 반환되어야 한다"""
invalid_user = {"name": "Test User", "email": "not-an-email"}
response = requests.post(f"{BASE_URL}/users", json=invalid_user)
assert response.status_code in [201, 400, 422]
print(f"\n✅ TC08 PASS | status: {response.status_code}")
def test_post_user_schema():
"""TC09: 응답 스키마(타입)가 올바르다"""
new_user = {"name": "Yoshitsugu Tester", "email": "yoshitsugu@example.com"}
response = requests.post(f"{BASE_URL}/users", json=new_user)
response.raise_for_status()
body = response.json()
assert isinstance(body["id"], int)
assert isinstance(body["name"], str)
print(f"\n✅ TC09 PASS | 스키마 검증 완료")실행 커맨드
pytest test_post_api.py -v -s실행 결과 샘플
test_post_api.py::test_post_user_status_code PASSED
test_post_api.py::test_post_user_raise_for_status PASSED
test_post_api.py::test_post_user_header PASSED
test_post_api.py::test_post_user_response_body PASSED
test_post_api.py::test_post_user_id_assigned PASSED
test_post_api.py::test_post_user_missing_required_fields PASSED
test_post_api.py::test_post_user_empty_string PASSED
test_post_api.py::test_post_user_invalid_email PASSED
test_post_api.py::test_post_user_schema PASSED
9 passed in 4.12s ✅06. 자주 겪는 문제 & 해결법
구현 중 실제로 겪었던 문제들을 정리했습니다. 같은 곳에서 막히는 분들께 도움이 되면 좋겠습니다.
① json=과 data=의 차이로 막힌다
requests로 POST 요청을 보낼 때 json= 과 data= 의 차이를 몰라서 막혔습니다. data= 를 사용하면 Content-Type이 application/x-www-form-urlencoded 가 되어버려 JSON API에서 실패합니다.
# ❌ data=를 사용하면 JSON 형식으로 전송되지 않음
response = requests.post(url, data={"name": "Taro"})
# ✅ json=를 사용하면 자동으로 Content-Type: application/json이 설정됨
response = requests.post(url, json={"name": "Taro"})💡 포인트:json= 을 사용하면 요청 바디 변환과 Content-Type 설정이 자동으로 이루어지므로 JSON API에는 반드시 json= 을 사용하세요.
② POST 상태 코드가 200인지 201인지 헷갈린다
POST 성공 응답이 200인지 201인지 헷갈렸습니다. API 설계에 따라 다르지만 REST API 관습에서는 신규 생성은 201이 일반적입니다.
# ❌ 고정으로 200을 기대해버린다
assert response.status_code == 200
# ✅ API 사양에 맞게 확인한다
# REST API 관습: 생성 성공은 201
assert response.status_code == 201
# 사양이 애매한 경우는 둘 다 허용하는 작성법도 가능
assert response.status_code in [200, 201]💡 포인트:테스트를 작성하기 전에 API 문서에서 기대하는 상태 코드를 확인하세요.
③ 응답 바디에 전송한 데이터가 포함되지 않는다
POST 응답에는 전송한 데이터가 그대로 반환될 것이라고 생각했지만, 서버 측에서 가공된 데이터가 반환되는 경우가 있었습니다.
# ❌ 전송 데이터와 완전 일치를 기대해버린다
assert body == new_user # 서버 측에서 id가 추가되므로 실패
# ✅ 필요한 필드만 개별로 검증한다
assert body["name"] == new_user["name"]
assert body["email"] == new_user["email"]
assert "id" in body # 서버가 부여한 ID도 확인💡 포인트:POST 응답에는 서버가 부여한 ID나 자동 생성 필드가 추가됩니다. 완전 일치가 아닌 필요한 필드를 개별로 검증하는 편이 견고합니다.
④ 유효성 검사 오류 상태 코드가 400인지 422인지 헷갈린다
유효성 검사 오류 시 상태 코드가 400인지 422인지 헷갈리는 경우가 있었습니다.
# ❌ 400만을 기대해버린다
assert response.status_code == 400
# ✅ API 사양에 따라 둘 다 허용한다
assert response.status_code in [400, 422]
# 400 Bad Request → 일반적인 요청 오류
# 422 Unprocessable → 유효성 검사 오류(FastAPI 등에서 많이 사용)💡 포인트:유효성 검사 오류 상태 코드는 프레임워크에 따라 다릅니다. Express는 400, FastAPI는 422를 반환하는 경우가 많습니다. API 문서로 확인하세요.
⑤ 목 API에서 유효성 검사를 검증할 수 없다
JSONPlaceholder에서 유효성 검사 테스트를 작성했더니 부정한 데이터를 보내도 201이 반환되어버려 테스트의 의미가 없어졌습니다.
# JSONPlaceholder는 목이므로
# 부정한 데이터를 보내도 201이 반환되어버린다
response = requests.post(url, json={"email": "not-an-email"})
print(response.status_code) # → 201(본래는 400이 기대됨)⚠️ 주의:유효성 검사를 정확하게 테스트하려면 실제 유효성 검사 기능을 가진 API가 필요합니다. Restful Booker나 reqres.in 등을 사용하거나 직접 만든 API로 검증하세요.
07. 자주 묻는 질문(FAQ)
Q. POST 테스트에서 최소한 확인해야 할 것은 무엇인가요?
A. 최소한 「상태 코드가 201인 것」「전송한 데이터가 응답에 포함되어 있는 것」「ID가 부여되어 있는 것」의 3가지입니다. 여유가 있다면 유효성 검사・스키마 검증・헤더 확인도 추가하면 실무 수준의 테스트가 됩니다.
Q. requests.post()로 JSON을 보내는 방법은?
A. requests.post(url, json=데이터) 를 사용합니다. json= 을 사용하면 Content-Type이 자동으로 application/json 으로 설정됩니다. data= 를 사용하면 폼 데이터로 전송되어버리므로 JSON API에는 반드시 json= 을 사용하세요.
Q. GET 테스트와 POST 테스트의 차이는 무엇인가요?
A. GET은 「데이터 취득」, POST는 「데이터 생성」입니다. POST 테스트는 요청 바디를 전송하는 점, 성공 시 상태 코드가 201인 점, 유효성 검사 오류 검증이 필요한 점이 GET과 다릅니다.
Q. 유효성 검사 테스트는 어느 API로 시험해볼 수 있나요?
A. JSONPlaceholder는 목 API이므로 유효성 검사 기능이 없습니다. 본격적인 유효성 검사 테스트에는 reqres.in 이나 직접 만든 API를 사용하세요. 포트폴리오용으로는 Restful Booker의 인증 엔드포인트도 사용할 수 있습니다.
Q. POST와 PUT의 차이는 무엇인가요?
A. POST는 「신규 생성」, PUT은 「기존 데이터의 전체 업데이트」입니다. POST는 매번 새로운 리소스가 생성되는 반면 PUT은 같은 요청을 몇 번 보내도 같은 결과가 됩니다(멱등성). 다음 글에서 PUT・PATCH 테스트를 해설합니다.
08. 정리
Python으로 API POST 테스트를 구현하는 경우, pytest와 requests를 사용해서 상태 코드・응답 바디・ID 부여・유효성 검사의 4가지를 검증하는 것이 실무의 기본입니다.
이 글에서는 pytest와 requests를 사용한 API POST 테스트 구현 방법을 해설했습니다.
| 테스트 케이스 | 내용 |
|---|---|
| TC01 | 상태 코드가 201이어야 한다 |
| TC02 | raise_for_status()로 4xx/5xx를 검지 |
| TC03 | Content-Type이 JSON이어야 한다 |
| TC04 | 전송 데이터가 응답에 올바르게 포함되어야 한다 |
| TC05 | 생성된 리소스에 ID가 부여되어야 한다 |
| TC06 | 필수 필드 없을 때 적절한 오류가 반환되어야 한다 |
| TC07 | 빈 문자열 전송 시 적절한 오류가 반환되어야 한다 |
| TC08 | 잘못된 이메일 형식 시 적절한 오류가 반환되어야 한다 |
| TC09 | 응답 스키마(타입)가 올바르다 |
다음 글에서는 PUT・PATCH 요청의 API 테스트(기존 데이터의 업데이트・부분 업데이트)를 해설합니다.

