여행서비스 E2E 테스트: Playwright MCP 활용
여행서비스 E2E 테스트: Playwright MCP 활용
1. 개요
- Playwright MCP(Model Context Protocol)를 활용한 E2E 테스트 작성 및 실행
- 여행서비스(국내숙소)의 주요 시나리오를 모바일 환경에서 테스트
- Cursor와 Playwright MCP를 연동하여 자연어 기반 테스트 시나리오 작성 및 자동화
2. 테스트 작성 규칙 및 절차
2.1 테스트 작성 절차
- 자연어 시나리오 입력: 프롬프트에 자연어로 테스트 시나리오를 입력
- 수동 실행 및 확인: 실제 브라우저에서 수동으로 실행하며 화면 요소와 동작을 확인
- 수동 실행 결과(스크린샷, 동영상, 주요 동작 로그 등)를 반드시 캡처하여 기록
- 수동 실행이 완료되어야만 feature 파일 및 테스트 코드 작성 가능
- Gherkin 시나리오 작성: 수동 실행 결과를 바탕으로 Gherkin 문법(.feature 파일)으로 시나리오 작성
- Playwright 테스트 코드 작성: 작성된 .feature 파일을 기반으로 playwright 테스트 코드 작성
2.2 시나리오 작성 규칙
- 시나리오는
.feature파일로 작성 - Gherkin 문법(Feature, Scenario, Given/When/Then) 사용
- 시나리오별로 명확한 설명과 목적 작성
- 실제 서비스 환경과 유사한 목 데이터 활용
- 한글 또는 영어로 의미가 명확하게 작성
- 예약어, 금지어, 혼동될 수 있는 이름 사용 금지
2.3 테스트 코드 작성 규칙
- 분할 시나리오 테스트 구조 원칙: 하나의 큰 e2e 시나리오를 여러 개의 독립적인 test로 분할
- 각 단계별로 pass/fail을 명확히 확인할 수 있도록 구성
test.beforeAll,test.afterAll에서 page를 생성/종료하고, 각 test에서 page를 재사용- 각 test는 독립적으로 실행 가능해야 하며, 시나리오별로 명확하게 구분
- 테스트 파일명은
.spec.ts확장자 사용 - 각 테스트는 시나리오와 1:1로 매핑되도록 작성
- 테스트 실행 전후로 필요한 초기화/정리 코드를 명확히 작성
- 외부 API 직접 호출 금지(목 데이터 활용)
- 테스트 내에서 임의로 타이머/딜레이 남용 금지
- 변수명, 함수명 등은 의미가 명확하게 작성
- 공통/반복 코드는 함수 또는 유틸로 분리
- URL에
/m/이 포함되어 있으면 모바일 에이전트로 간주하고, 갤럭시 S20(360x800) 뷰포트와 User-Agent를 적용 - Playwright 테스트 코드는 기본적으로 headed(브라우저 UI 표시) 모드로 실행하는 것을 원칙으로 함
3. 구현된 테스트 시나리오
3.1 여행서비스 진입 시나리오
Gherkin 시나리오:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Feature: 여행서비스 주요 시나리오
Scenario: 로그인 성공
Given 사용자가 여행서비스 페이지에 모바일에이전트(갤럭시20)로 접속한다.
And 하단에 사람모양 아이콘을 클릭하여 로그인을 시도한다
When 아이디(test.user@example.com)와 비밀번호(testPassword123)를 입력하고 로그인 버튼을 클릭한다
Then 마이페이지 화면으로 이동하며 "사용자님" 메시지가 보인다
Scenario: 홈으로 이동
Given 하단 가운데 홈 버튼을 클릭하여 홈화면으로 이동한다
Then 메인화면이 보인다
Scenario: 여행서비스(국내숙소) 이동
Given 사용자가 메인 페이지에 접속한다
When 여행진입 버튼을 누른다
Then 좌측상단에 "국내숙소"가 표시된다
시나리오 요약:
- 로그인 성공: 모바일 접속 → 사람 아이콘 클릭 → 로그인 → “사용자님” 확인
- 홈으로 이동: 하단 홈 버튼 클릭 → 메인 화면 확인
- 여행서비스(국내숙소) 이동: 메인 페이지 → 여행진입 버튼 → “국내숙소” 표시 확인
테스트 코드 특징:
- 파일:
playwright_mcp/steps/travel.spec.ts - 모바일 설정: 갤럭시 S20 (360x800)
- 분할 시나리오 구조로 각 단계를 독립 테스트로 구성
- 마지막에
travel-service.png캡처
테스트 코드:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import { test, expect } from "@playwright/test";
test.describe("여행서비스 주요 시나리오", () => {
test.use({
viewport: { width: 360, height: 800 },
userAgent: "Mozilla/5.0 (Linux; Android 10; SM-G981N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.94 Mobile Safari/537.36"
});
let page;
test.beforeAll(async ({ browser }) => {
page = await browser.newPage();
});
test.afterAll(async () => {
await page.close();
});
test("로그인 성공", async () => {
await page.goto("https://example.com/m/display/main");
await page.locator('a[href*="mypage"], a:has-text("마이페이지")').first().click();
await page.getByPlaceholder("아이디").or(page.getByLabel("아이디")).fill("test.user@example.com");
await page.getByPlaceholder("비밀번호").or(page.getByLabel("비밀번호")).fill("testPassword123");
await page.getByRole("button", { name: /로그인/ }).click();
await expect(page.getByText(/사용자\s*님/)).toBeVisible();
});
test("홈으로 이동", async () => {
await page.locator('a[data-cmpnt-typ="actionbar"][data-cmpnt-name="home"]').click();
await page.waitForLoadState("networkidle");
await expect(page.getByText(/메인|홈/)).toBeVisible();
});
test("여행서비스(국내숙소) 이동 및 캡처", async () => {
await page.goto("https://example.com/m/display/main");
await page.locator('a:has-text("여행진입")').first().click();
await expect(page.getByText(/국내숙소/)).toBeVisible();
await page.waitForLoadState("networkidle");
await page.screenshot({ path: "travel-service.png", fullPage: true });
});
});
3.2 여행서비스 주요 시나리오 (travel_search 모듈 포함)
Gherkin 시나리오:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
Feature: 여행서비스 주요 시나리오
Scenario: 로그인 성공
Given 사용자가 여행서비스 페이지에 모바일에이전트(갤럭시20)로 접속한다.
And 하단에 사람모양 아이콘을 클릭하여 로그인을 시도한다
When 아이디(test.user@example.com)와 비밀번호(testPassword123)를 입력하고 로그인 버튼을 클릭한다
Then 마이페이지 화면으로 이동하며 "사용자님" 메시지가 보인다
Scenario: 홈으로 이동
Given 하단 가운데 홈 버튼을 클릭하여 홈화면으로 이동한다
Then 메인화면이 보인다
Scenario: 여행서비스(국내숙소) 이동
Given 사용자가 메인 페이지에 접속한다
When 여행진입 버튼을 누른다
Then 좌측상단에 "국내숙소"가 표시된다
Scenario: 여행서비스 travel_search 모듈 노출 확인
Given 사용자가 여행서비스페이지에 접속한다
When 메인 페이지가 랜더링이 완료된다
Then "travel_search" 모듈이 화면에 노출된다
And 모듈 내 "어디로 떠나세요?", "국내 여행지를 알려주세요" 문구가 보인다
Scenario: 여행서비스 travel_search 모듈 클릭 동작 확인
Given 사용자가 여행서비스페이지에서 travel_search 모듈을 확인한다
When travel_search 모듈의 "국내 여행지를 알려주세요" 문구를 클릭한다
Then 여행서비스 지역 페이지로 이동한다
And 모듈 내 "지역을 알려주세요." 문구가 보인다
And "서울"문구가 볼드처리로 디폴트 선택되어 있다
시나리오 요약:
- 로그인 성공
- 홈으로 이동
- 여행서비스(국내숙소) 이동
- travel_search 모듈 노출 확인: “어디로 떠나세요?”, “국내 여행지를 알려주세요” 문구 확인
- travel_search 모듈 클릭 동작 확인: “국내 여행지를 알려주세요” 클릭 → 지역 페이지 이동 → “지역을 알려주세요” 확인 → “서울” 볼드 디폴트 선택 확인
테스트 코드 특징:
- 5개 독립 테스트로 분할
- travel_search 모듈 테스트 포함
- 마지막에
travelmn-final.png캡처
테스트 코드:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import { test, expect } from "@playwright/test";
test.use({
viewport: { width: 360, height: 800 },
userAgent: "Mozilla/5.0 (Linux; Android 10; SM-G981N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.94 Mobile Safari/537.36"
});
test.describe("여행서비스 주요 시나리오 + travel_search", () => {
let page;
test.beforeAll(async ({ browser }) => {
page = await browser.newPage();
});
test.afterAll(async () => {
await page.close();
});
test("로그인 성공", async () => {
await page.goto("https://example.com/m/display/main");
await page.locator('a[href*="mypage"], a:has-text("마이페이지")').first().click();
await page.getByPlaceholder("아이디").or(page.getByLabel("아이디")).fill("test.user@example.com");
await page.getByPlaceholder("비밀번호").or(page.getByLabel("비밀번호")).fill("testPassword123");
await page.getByRole("button", { name: /로그인/ }).click();
await expect(page.getByText(/사용자\s*님/)).toBeVisible();
});
test("홈으로 이동", async () => {
await page.locator('a[data-cmpnt-typ="actionbar"][data-cmpnt-name="home"]').click();
await page.waitForLoadState("networkidle");
await expect(page.getByText(/메인|홈/)).toBeVisible();
});
test("여행서비스(국내숙소) 이동", async () => {
await page.goto("https://example.com/m/display/main");
await page.locator('a:has-text("여행진입")').first().click();
await expect(page.getByText(/국내숙소/)).toBeVisible();
await page.waitForLoadState("networkidle");
});
test("travel_search 모듈 노출 확인", async () => {
await expect(page.getByText(/어디로 떠나세요/)).toBeVisible();
await expect(page.getByRole("button", { name: /국내 여행지를 알려주세요/ })).toBeVisible();
});
test("travel_search 모듈 클릭 동작 확인 및 최종 화면 캡처", async () => {
await page.getByRole("button", { name: /국내 여행지를 알려주세요/ }).click();
await expect(page.getByText(/지역을 알려주세요/)).toBeVisible();
await expect(page.getByRole("tab", { name: /서울/ })).toHaveAttribute("aria-selected", "true");
await page.screenshot({ path: "travelmn-final.png", fullPage: true });
});
});
3.3 국내 숙소 모바일 예약 시나리오
Gherkin 시나리오:
1
2
3
4
5
6
7
8
9
10
11
12
13
Feature: 국내 숙소 모바일 예약 시나리오
Scenario: 서울 전체보기, 날짜/인원/호텔/예약가능 필터 적용
Given 사용자는 갤럭시 S20 모바일 환경에서 국내숙소 메인 페이지에 접속한다
When "국내 여행지를 알려주세요" 버튼을 클릭한다
And "서울 전체보기"를 클릭한다
And 달력 아이콘 영역에서 날짜/인원 선택 버튼을 클릭한다
And 달력에서 체크인 날짜로 2025년 7월 31일, 체크아웃 날짜로 2025년 8월 5일을 선택한다
And 하단 "적용하기" 버튼을 클릭한다
And 인원 변경 UI에서 성인 3명, 아동 1명을 선택 후 "적용하기" 버튼을 클릭한다
And 숙소유형탭에서 "호텔"을 선택한다
And "예약가능" 체크박스를 선택한다
Then 호텔 숙소 리스트가 예약가능 조건으로 노출된다
시나리오 요약: 서울 전체보기, 날짜/인원/호텔/예약가능 필터 적용
- 국내숙소 메인 접속 → “국내 여행지를 알려주세요” 클릭 → “서울 전체보기” 선택
- 날짜 선택: 체크인 2025-07-31, 체크아웃 2025-08-05
- 인원: 성인 3명, 아동 1명
- 숙소유형: 호텔 선택
- 예약가능 체크박스 선택
- 호텔 숙소 리스트 노출 확인
테스트 코드 특징:
- 4개 독립 테스트로 분할:
- 메인 진입 및 지역 선택
- 날짜/인원 선택 진입 및 날짜 선택
- 인원 변경(성인 3, 아동 1)
- 숙소유형탭 호텔 선택 및 예약가능 체크
테스트 코드:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import { test, expect } from "@playwright/test";
test.use({
viewport: { width: 360, height: 800 },
userAgent: "Mozilla/5.0 (Linux; Android 10; SM-G981N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Mobile Safari/537.36"
});
test.describe("국내 숙소 모바일 예약 시나리오", () => {
let page;
test.beforeAll(async ({ browser }) => {
page = await browser.newPage();
});
test.afterAll(async () => {
await page.close();
});
test("메인 진입 및 지역 선택", async () => {
await page.goto("https://example.com/display/shop/accommodation");
await expect(page).toHaveTitle(/국내숙소/);
await page.getByRole("button", { name: /국내 여행지를 알려주세요/ }).click();
await expect(page.getByText(/지역을 알려주세요/)).toBeVisible();
await page.getByRole("link", { name: /서울 전체보기/ }).click();
await expect(page.getByRole("button", { name: /서울 전체보기/ })).toBeVisible();
});
test("날짜/인원 선택 진입 및 날짜 선택", async () => {
await page.getByRole("button", { name: /날짜 인원을 변경하려면 선택하세요/ }).click();
await expect(page.getByText(/날짜 선택/)).toBeVisible();
await page.getByRole("button", { name: /2025년 7월 31일/ }).click();
await page.getByRole("button", { name: /2025년 8월 5일/ }).click();
await page.getByRole("button", { name: /^적용하기$/ }).click();
await expect(page.locator('button:has-text("07.31")')).toBeVisible();
});
test("인원 변경(성인 3, 아동 1)", async () => {
await page.getByRole("button", { name: /날짜 인원을 변경하려면 선택하세요/ }).click();
await page.getByRole("button", { name: /인원 변경/ }).click();
const 성인증가 = page.locator('strong:has-text("성인")').locator("..").getByRole("button", { name: "+" });
const 아동증가 = page.locator('strong:has-text("아동")').locator("..").getByRole("button", { name: "+" });
await 성인증가.click();
await 아동증가.click();
await page.getByRole("button", { name: /^적용하기$/ }).click();
await expect(page.locator("text=/성인\\s*3/")).toBeVisible();
});
test("숙소유형탭 호텔 선택 및 예약가능 체크", async () => {
await page.getByRole("tab", { name: "호텔" }).click();
await page.getByRole("checkbox", { name: "예약가능" }).check();
await expect(page.getByRole("tab", { name: "호텔", selected: true })).toBeVisible();
});
});
3.4 국내숙소 예약 시나리오 (갤럭시20 모바일)
Gherkin 시나리오:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Feature: 국내숙소 예약 시나리오 (갤럭시20 모바일)
Scenario: 호텔/예약가능/날짜/인원/필터 적용
Given 갤럭시20 모바일 에이전트로 국내숙소 메인에 접속한다
When "국내 여행지를 알려주세요" 버튼을 클릭한다
And "서울 전체보기"를 클릭한다
And 날짜/인원 선택 버튼을 클릭한다
And 달력에서 체크인 날짜를 2025-07-31, 체크아웃 날짜를 2025-08-05로 선택한다
And 인원 변경 버튼을 클릭한다
And 성인 3명, 아동 1명으로 변경한다
And 적용하기 버튼을 클릭한다
And 숙소유형탭에서 "호텔"을 선택한다
And 예약가능 체크박스를 선택한다
Then 결과 화면이 렌더링되고, 주요 요소(호텔탭, 예약가능, 날짜/인원 버튼)가 노출된다
And 화면을 캡처한다
시나리오 요약: 호텔/예약가능/날짜/인원/필터 적용
- 갤럭시20 모바일로 국내숙소 메인 접속
- “국내 여행지를 알려주세요” → “서울 전체보기” 선택
- 날짜 선택: 체크인 2025-07-31, 체크아웃 2025-08-05
- 인원: 성인 3명, 아동 1명
- 숙소유형: 호텔 선택
- 예약가능 체크박스 선택
- 결과 화면 렌더링 및 주요 요소 노출 확인
- 화면 캡처
테스트 코드 특징:
- 단일 통합 테스트로 작성
- 갤럭시20 User-Agent 설정
- 마지막에
travel-final.jpg캡처 - 주요 요소 검증 포함 (호텔 탭, 예약가능 체크박스, 날짜 정보)
테스트 코드:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import { test, expect } from "@playwright/test";
test.use({ viewport: { width: 360, height: 800 } });
test.describe("국내숙소 예약 시나리오", () => {
test("호텔/예약가능/날짜/인원/필터 적용", async ({ page, context }) => {
await context.setExtraHTTPHeaders({
"user-agent": "Mozilla/5.0 (Linux; Android 10; SM-G981N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36"
});
await page.goto("https://example.com/display/shop/accommodation");
await page.getByRole("button", { name: "국내 여행지를 알려주세요" }).click();
await page.getByRole("link", { name: "서울 전체보기" }).click();
await page.getByRole("button", { name: /날짜 인원을 변경하려면 선택하세요/ }).click();
await page.getByRole("button", { name: "2025년 7월 31일 목요일" }).click();
await page.getByRole("button", { name: "2025년 8월 5일 화요일" }).click();
await page.getByRole("button", { name: "인원 변경" }).click();
const 성인증가 = page.locator("button", { hasText: "+" }).nth(0);
const 아동증가 = page.locator("button", { hasText: "+" }).nth(1);
await 성인증가.click();
await 아동증가.click();
await page.getByRole("button", { name: "적용하기" }).click();
await page.getByRole("tab", { name: "호텔" }).click();
await page.getByRole("checkbox", { name: "예약가능" }).check();
await page.screenshot({ path: "travel-final.jpg", fullPage: true });
await expect(page.getByRole("tab", { name: "호텔", selected: true })).toBeVisible();
await expect(page.getByRole("checkbox", { name: "예약가능", checked: true })).toBeVisible();
await expect(page.locator('[aria-label*="체크인 2025년 7월 31일 체크아웃 2025년 8월 5일"]')).toBeVisible();
});
});
4. 개발 과정 및 특징
4.1 모바일 환경 설정
- 갤럭시 S20 환경: 360x800 뷰포트, 모바일 User-Agent 설정
- URL에
/m/이 포함된 경우 자동으로 모바일 에이전트로 간주
4.2 테스트 구조
- 분할 시나리오 테스트 구조: 각 단계를 독립적인 test로 분할하여 pass/fail을 명확히 확인
test.beforeAll,test.afterAll에서 page 생성/종료- 각 test에서 page를 재사용하여 시나리오 연속성 유지
4.3 화면 캡처
- 각 시나리오 마지막에 스크린샷 저장
fullPage: true옵션으로 전체 페이지 캡처
5. 결과 & 배운 점
- 자연어 기반 시나리오 작성으로 테스트 작성 시간 단축
- 분할 시나리오 테스트 구조로 각 단계별 디버깅 용이
- 모바일 환경에서의 실제 사용자 시나리오 검증 가능
- Playwright MCP를 통한 AI 기반 테스트 자동화의 효율성 확인
- 수동 실행 후 테스트 코드 작성으로 정확도 향상
6. 기술 스택 (Tech Stack)
Cursor Playwright Playwright MCP Gherkin TypeScript
7. 참고 자료
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.