Playwright + Python으로 인증 플로우 API 테스트를 자동화하는 방법|토큰 취득・CRUD・보안 검증

테스트 자동화

📌 이런 분께 추천합니다

  • Playwright의 APIRequestContext를 사용한 API 테스트를 시작하고 싶은 분
  • 인증 플로우(토큰 취득·전달·거부)의 테스트 구현에 관심 있는 분
  • pytest의 fixture를 활용하여 테스트 데이터를 효율적으로 관리하고 싶은 분
  • REST API의 CRUD 조작(POST·PUT·DELETE)을 자동 테스트하고 싶은 분

✅ 이 글을 읽으면 알 수 있는 것

  • Playwright의 APIRequestContext로 API 테스트를 구현하는 방법
  • 인증 토큰을 fixture로 관리하여 테스트 간에 공유하는 패턴
  • GET / POST / PUT / DELETE 각 HTTP 메서드의 테스트 구현
  • 인증 없는 접근을 올바르게 거부하는 것을 자동 테스트하는 방법

👤

글쓴이 소개:QA 엔지니어로서 Playwright·Python·pytest를 사용한 API 테스트 자동화를 실무에서 담당. 이 글에서 사용하는 코드는 모두 GitHub에서 공개하고 있습니다. GitHub에서 코드 보기 →

API 테스트라고 하면 「requests 라이브러리 + pytest」의 조합이 유명하지만, 사실 Playwright에도 APIRequestContext라는 강력한 API 테스트 기능이 탑재되어 있습니다.

이 글에서는 호텔 예약 API 연습 사이트 Restful Booker를 사용하여, 인증 플로우(로그인→토큰 취득→인증 포함 리퀘스트)를 6가지 테스트 케이스로 자동화하는 구현 예시를 해설합니다.


대상 API・테스트 구성

사용하는 API

항목내용
대상 사이트Restful Booker(호텔 예약 API 연습 사이트)
BASE URLrestful-booker.herokuapp.com
프레임워크Playwright(Python)+ pytest
인증 방식토큰 인증(Cookie: token=xxx)

6가지 테스트 케이스

TCHTTP 메서드내용기대 상태
TC01POST /auth정상 로그인 → 토큰 취득200
TC02POST /auth비정상 로그인(오류 비밀번호)→ 에러 확인200 + reason
TC03POST /booking토큰을 사용하여 예약 생성200
TC04PUT /booking/{id}토큰을 사용하여 예약 업데이트200
TC05DELETE /booking/{id}토큰을 사용하여 예약 삭제201
TC06DELETE /booking/{id}토큰 없이 삭제 → 거부 확인403

환경 구축

필요한 패키지 설치

# Playwright + pytest 설치
pip install playwright pytest pytest-playwright pytest-html

# Playwright 브라우저 설치(API 테스트만이라면 생략 가능)
playwright install
💡 포인트: Playwright의 APIRequestContext는 브라우저를 기동하지 않습니다. API 테스트만이라면 playwright 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.htmlreport.html이라는 이름으로 HTML 리포트를 생성한다
--self-contained-htmlCSS나 이미지를 HTML에 내장. 파일 1개로 완결되어 공유하기 쉽다
# pytest.ini가 있으면 추가 옵션 불필요 — 자동으로 HTML 리포트가 생성된다
pytest test_auth_flow.py -v -s
메리트: 매번 커맨드에 --html=report.html을 입력할 필요가 없어집니다. 팀 전체에서 설정이 통일됩니다.

report.html이 제대로 표시되지 않을 때의 대처법

report.html을 더블클릭으로 직접 브라우저에서 열면 보안 제한(CORS)으로 스타일이 무너지거나 내용이 표시되지 않는 경우가 있습니다. 그런 경우 Python 내장 서버를 사용하여 로컬에서 호스팅하는 방법이 유효합니다.

▼ 순서

STEP조작 내용
1report.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.html
확인할 수 있는 것: 테스트의 PASS / FAIL 일람·실행 시간·에러 상세·print()로 출력한 내용(-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}"}
💡 포인트: 테스트하는 API의 문서에서 「어떤 방식으로 토큰을 전달하는가」를 반드시 확인합시다. 방식을 잘못 사용하면 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이 기대값과 다릅니다"
💡 실무 Tip: 이 4가지 패턴(Context 작성→토큰 취득→헤더 설정→리퀘스트&검증)이 몸에 배면 어떤 REST API의 테스트에도 응용할 수 있습니다. 이후의 코드는 모두 이 패턴의 조합입니다.

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 token
💡 인증 토큰은 세션에서 1회만 취득하여 TC03〜TC06 전체에서 재사용합니다. 이것이 DRY 원칙의 실천입니다.

fixture③:테스트용 예약 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
💡 예약 ID는 세션 시작 시에 1회만 작성합니다. TC03(작성 확인)→TC04(업데이트)→TC05(삭제)→TC06(삭제 후 인증 에러 확인)의 흐름으로 재사용합니다.

테스트 코드 전문

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']}")
검증 포인트: 상태 200 / 리스폰스에 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}")
⚠️ 주의 포인트: 이 API는 인증 실패 시에도 상태 200을 반환합니다(설계 사양). 대신 리스폰스 바디의 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}")
검증 포인트: 상태 200 / firstname이 “Hanako”로 업데이트 / totalprice가 15000으로 업데이트

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})")
⚠️ 주의 포인트: Restful Booker의 DELETE는 성공 시에 상태 201을 반환합니다. 일반적인 REST 설계에서는 200 또는 204가 많습니다만, 이 API의 사양입니다. 테스트하는 API의 사양서를 반드시 확인합시다.

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}")
검증 포인트: 인증 없는 접근에 대해 상태 403이 반환될 것. 보안 테스트의 기본 패턴입니다.

실행 방법과 결과

테스트 실행

# 통상 실행
pytest test_auth_flow.py -v

# print 출력도 표시하는 경우
pytest test_auth_flow.py -v -s

실행 결과(터미널)

실제로 실행한 결과입니다. 6테스트 전부 PASSED,합계 5.07초로 완료됐습니다.

pytest 실행 결과 터미널 - 6 passed in 5.07s

▲ 터미널에서의 실행 결과 — TC01〜TC06 전부 PASSED

HTML 리포트(report.html)

pytest.ini의 설정으로 자동 생성된 report.html을 브라우저에서 연 화면입니다.

report.html - 6 Passed

▲ report.html 표시 결과 — 0 Failed / 6 Passed / 합계 5초


설계 포인트:왜 이 구조로 했는가

🔑
fixture로 토큰을 공유

매 테스트마다 로그인을 다시 하지 않고, 세션에서 1번만 취득. DRY 원칙의 실천.

🏗
fixture 의존 관계의 활용

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 테스트를 같은 프레임워크로 통일할 수 있는 것도 큰 메리트입니다.

제목과 URL을 복사했습니다