📌 이런 분께 추천합니다
- Playwright의 APIRequestContext를 사용한 API 테스트를 시작하고 싶은 분
- 인증 플로우(토큰 취득·전달·거부)의 테스트 구현에 관심 있는 분
- pytest의 fixture를 활용하여 테스트 데이터를 효율적으로 관리하고 싶은 분
- REST API의 CRUD 조작(POST·PUT·DELETE)을 자동 테스트하고 싶은 분
✅ 이 글을 읽으면 알 수 있는 것
- Playwright의 APIRequestContext로 API 테스트를 구현하는 방법
- 인증 토큰을 fixture로 관리하여 테스트 간에 공유하는 패턴
- GET / POST / PUT / DELETE 각 HTTP 메서드의 테스트 구현
- 인증 없는 접근을 올바르게 거부하는 것을 자동 테스트하는 방법
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 pytest-html
# Playwright 브라우저 설치(API 테스트만이라면 생략 가능)
playwright installplaywright install은 생략할 수 있지만, 향후 E2E 테스트와 조합할 경우를 고려하여 설치해 두면 편리합니다.pytest.ini:HTML 리포트를 자동 생성한다
프로젝트 루트에 pytest.ini를 두면, 테스트 실행마다 HTML 리포트가 자동으로 생성됩니다. 테스트 결과를 브라우저에서 확인할 수 있게 되어, 에비던스로도 활용할 수 있습니다.
▼ 폴더 구성
project/
├── pytest.ini # ← 여기에 둔다
├── test_auth_flow.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개로 완결되어 공유하기 쉽다 |
# pytest.ini가 있으면 추가 옵션 불필요 — 자동으로 HTML 리포트가 생성된다
pytest test_auth_flow.py -v -s--html=report.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 ...# STEP 3:브라우저에서 이 URL을 연다(URL 바에 직접 입력)
localhost:8080/report.htmlprint()로 출력한 내용(-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}"}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이 기대값과 다릅니다"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 tokenfixture③:테스트용 예약 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테스트 코드 전문
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']}")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}")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}")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})")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}")실행 방법과 결과
테스트 실행
# 통상 실행
pytest test_auth_flow.py -v
# print 출력도 표시하는 경우
pytest test_auth_flow.py -v -s실행 결과(터미널)
실제로 실행한 결과입니다. 6테스트 전부 PASSED,합계 5.07초로 완료됐습니다.

▲ 터미널에서의 실행 결과 — TC01〜TC06 전부 PASSED
HTML 리포트(report.html)
pytest.ini의 설정으로 자동 생성된 report.html을 브라우저에서 연 화면입니다.

▲ report.html 표시 결과 — 0 Failed / 6 Passed / 합계 5초
설계 포인트:왜 이 구조로 했는가
매 테스트마다 로그인을 다시 하지 않고, 세션에서 1번만 취득. DRY 원칙의 실천.
booking_id fixture가 auth_token에 의존하는 설계로 전제 조건을 자동 보증.
TC06에서 「거부될 것」을 검증. 정상계뿐만 아니라 이상계·보안도 자동화.
APIRequestContext는 브라우저를 기동하지 않아 6테스트가 약 5초에 완료.
📖 관련 기사
정리
이 글에서는 Playwright의 APIRequestContext와 pytest를 사용하여 인증 플로우를 6가지 테스트 케이스로 자동화하는 방법을 해설했습니다.
📋 이 글의 정리
- Playwright의 APIRequestContext는 브라우저 불필요로 고속인 API 테스트를 작성할 수 있다
- 인증 토큰은
scope="session"의 fixture로 관리함으로써 전체 테스트에서 공유할 수 있다 - POST·PUT·DELETE의 각 HTTP 메서드의 구현 패턴을 습득했다
- 이상계(오류 비밀번호)·보안(토큰 없음 거부)의 테스트도 자동화할 수 있다
- API에 따라 상태 코드의 사양이 다르기 때문에, 반드시 API 문서를 확인한다
Playwright는 E2E 테스트뿐만 아니라 API 테스트에도 대응하고 있습니다. E2E와 API 테스트를 같은 프레임워크로 통일할 수 있는 것도 큰 메리트입니다.

