들어가며
지난 글에서 Plan 도메인의 CRUD API를 모두 구현했다.
이제 과제를 시작하기 전에 진행했었던 Playwright MCP를 에이전트에 붙여 E2E테스트 자동화 하는 과정을 현재 앱에 적용하여 보겠다.
Playwright
Playwright는 마이크로소프트에서 만든 브라우저 자동화 도구다. 원래는 웹 페이지를 자동으로 클릭하고, 폼을 채우고 스크린샷을 찍는 등의 E2E 테스트를 위해 만들어졌다.
Selenium이나 Puppeteer 같은 도구와 비슷하지만 다음과 같은 몇 가지 장점이 있다.
- 빠른 실행 속도 - 다른 도구들보다 빠르다
- 안정적인 테스트 - 요소가 나타날 때까지 자동으로 기다려준다
- 다양한 브라우저 지원 - Chromium, Firefox, WebKit 모두 지원
- API 테스트 가능 - 브라우저 없이도 HTTP 요청을 보낼 수 있다
플레이라이트는 request fixture라는 걸 제공해서, 실제 브라우저를 띄우지 않고도 API 테스트를 할 수 있다.
Playwright 설치 과정 및 에이전트 MCP 설정
npm install -D @playwright/test
위 명령어로 playwright를 설치해준다.
설치가 끝났으면 Playwright 브라우저도 설치해야 한다.
npx playwright install
이 명령어를 실행하면 Chromium, Firefox, WebKit 브라우저가 자동으로 설치된다.
MCP 서버 설정
Playwright MCP를 클로드 코드에 연결하려면 설정 파일이 필요하다.
우선 프로젝트 루트에 .claude 폴더를 만들고, 그 안에 mcp.json 파일을 생성해준다.
그 다음으로 아래와 같이 작성해준다.
{
"mcpServers": {
"playwright": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-playwright"]
}
}
}
이 설정 파일은 클로드 코드 에이전트에게 playwright mcp 서버를 npx로 실행해주라고 알려주는 역할이다.
그리고 클로드 코드를 재시작 후 /mcp 명령어를 입력해서 플레이라이트 mcp 서버와 연결되었는지 확인해준다.
Playwright MCP - connected
연결이 되었다면 위와같이 나올 것이다.
이 후 클로드 코드에게 다음과 같이 프롬프트를 주었다.
내 fastify 서버는 http://localhost:3000에서 돌고 있고,
POST /api/plans, GET /api/plans, GET /api/plans/:id,
PATCH /api/plans/:id, DELETE /api/plans/:id 이런 엔드포인트가 있어.
playwright mcp 서버를 활용하고 request 픽스처를 이용해서
성공/실패 + zod 검증 케이스까지 포함한 테스트 파일을
tests/plan.spec.ts로 만들어줘.
그 결과로 만들어진 코드를 아래에서 확인해보자.
테스트 코드 구조
// plan.spec.ts
import { test, expect } from '@playwright/test';
const BASE_URL = 'http://localhost:3000';
test.describe('Plan API 테스트', () => {
let createdPlanId: string;
test.describe('POST /api/plans - 계획 생성', () => {
test('성공: 계획을 생성한다', async ({ request }) => {
const response = await request.post(`${BASE_URL}/api/plans`, {
data: {
title: '제주도 여행',
startDate: '2025-12-01',
endDate: '2025-12-05',
region: '제주도',
visible: true,
copyAllowed: false
}
});
expect(response.status()).toBe(201);
const data = await response.json();
expect(data).toHaveProperty('id');
expect(data.title).toBe('제주도 여행');
expect(data.startDate).toBe('2025-12-01');
expect(data.endDate).toBe('2025-12-05');
expect(data.region).toBe('제주도');
expect(data.visible).toBe(true);
expect(data.copyAllowed).toBe(false);
expect(data.bookmarkCount).toBe(0);
expect(data.likeCount).toBe(0);
expect(data).toHaveProperty('createdAt');
// 다음 테스트를 위해 ID 저장
createdPlanId = data.id;
});
});
에이전트가 생성해준 테스트코드 중 일부분인 계획생성 API 테스트코드이다.
request fixture로 요청을 하고 data 객체에 바디 데이터를 담아 응답된 데이터를 response에 저장한다.
그후 expect()로 검증을하게 되는데 이 부분은 스프링의 MockMVC와 많이 비슷했다. 아니 어쩌면 거의 같은 것 같다.
그런데 junit에서는 예시데이터 (요청데이터)를 생성하고 그 데이터로 서비스 로직을 호출하여 반환 값을 스터빙해야하는데, playwright로 작성된 코드에서는 그런 부분이 전혀 없다는게 엄청 편한 것 같다.
커서 페이지네이션 테스트
조금 복잡했던 커서 페이지네이션 테스트이다. 제대로 동작하는지 확인하고 싶었다.
테스트 시나리오
- 5개의 계획 생성
- 첫 페이지 조회 (size=2)
- nextCursor를 사용해서 두 번째 페이지 조회
- 첫 페이지와 두 번째 페이지의 아이템이 달라야 함
코드는 아래와 같다.
test('성공: 커서 페이지네이션', async ({ request }) => {
// 첫 페이지
const firstResponse = await request.get(`${BASE_URL}/api/plans?size=2`);
const firstData = await firstResponse.json();
expect(firstData.items.length).toBe(2);
expect(firstData.hasNext).toBe(true);
expect(firstData.nextCursor).not.toBeNull();
// 두 번째 페이지
const cursorParam = encodeURIComponent(JSON.stringify(firstData.nextCursor));
const secondResponse = await request.get(`${BASE_URL}/api/plans?cursor=${cursorParam}&size=2`);
const secondData = await secondResponse.json();
expect(secondResponse.status()).toBe(200);
expect(secondData.items.length).toBeGreaterThan(0);
// 첫 페이지와 두 번째 페이지의 아이템이 달라야 함
const firstIds = firstData.items.map((item: any) => item.id);
const secondIds = secondData.items.map((item: any) => item.id);
expect(firstIds).not.toEqual(secondIds);
});
먼저 size=2로 첫 페이지를 요청해 items, hasNext, nextCursor가 정상적으로 내려오는지 확인하는 것부터 시작한다.
이후 서버가 내려준 nextCursor를 JSON 문자열로 변환해 URL 인코딩한 뒤 두 번째 페이지를 요청하고, 응답 상태와 아이템 개수를 검증한다.
마지막으로 첫 페이지와 두 번째 페이지의 id 목록을 비교하여 두 페이지가 서로 겹치지 않는지 확인함으로써 커서 이동이 정상적으로 동작하는지 체크하는 과정이다.
문제점 - 잘못된 cursor 형식
모든 테스트 코드중에 딱 하나의 테스트만 실패했다. 해당 코드는 아래와 같다.
test('실패: 잘못된 cursor 형식', async ({ request }) => {
const response = await request.get(`${BASE_URL}/api/plans?cursor=invalid-json`);
expect(response.status()).toBe(400); // 기대값
});
응답 코드의 기댓값은 400번인데 실제로는 500 이 응답되었다.
뭐가 문제일까 코드를 살펴보니 plan.schema.ts의 cursor 검증 부분이 문제였다.
원인
cursor: z.string()
.transform((raw) => {
try {
return JSON.parse(raw);
} catch {
throw new Error('cursor는 JSON 형식이어야 합니다.'); // 일반 Error
}
})
throw new Error()를 사용하면 Zod 검증 에러가 아니라서, Fastify가 500 에러로 처리한다는 걸 알았다.
해결
Zod의 ctx.addIssue()를 사용해서 제대로 된 검증 에러로 만들어야 한다.
// 수정된 코드
cursor: z.string()
.transform((raw, ctx) => {
try {
return JSON.parse(raw);
} catch {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'cursor는 JSON 형식이어야 합니다.',
});
return z.NEVER;
}
})
ctx.addIssue() - zod 검증 에러 등록하는 부분이다.z.NEVER - 반환으로 검증 실패를 명시한다.
이렇게 수정하고 해당 테스트를 다시 돌려보니 400 에러가 제대로 응답되며 통과했다.
전체 테스트 실행
모든 테스트를 작성하고 나서 실행했다.
npx playwright test tests/plans.spec.ts --reporter=html
테스트 결과를 웹에서 확인할 수 있도록 –reporter=html 을 붙여준다.
으로 전환하기(5) - Playwright로 E2E 테스트 자동화-1763106689500.png)
테스트 결과를 위 이미지와 같이 볼 수 있고 각 테스트케이스마다 클릭하면 아래 이미지와 같이 굉장히 자세하게 확인 할 수 있다.
으로 전환하기(5) - Playwright로 E2E 테스트 자동화-1763106777229.png)
마치며
과제를 통해 처음으로 node.js 환fastify 기반 백엔드를 만들어봤다.
솔직히 스프링을 메인으로만 써왔던 입장이지만, 기존에 현재 회사에서 express로 api 구축을 해본 경험이 있기에 그렇게 어렵지는 않았다. express랑 구조가 서로 많이 비슷했다. 또 타입스크립트 같은 경우에는 예전에 잠깐 리액트를 배울 때 문법 공부를 해두었기에 이 부분도 크게 어려움은 없었지만, 유틸리티 타입 (Omit, Partial 등)과 같이 기억이 잘 안났던 부분이 있었기에 구글링과 GPT에게 물어보며 진행을 했다.
그리고 클로드 코드 같은 에이전트는 그냥 일반적인 프롬프트를하면서만 사용해왔지, mcp를 붙여본 경험은 이번이 처음이었다.
이번에 playwright mcp를 연동하는 과정에서 연동이 생각보다 엄청 간단했기 때문에 앞으로 다른 mcp도 많이 써볼까 생각한다.