1편에서 E2E로 다이얼로그 여닫기 같은 UI 동작을 눌러 봤다면,
2편은 "페이지를 타고 들어가서 API까지 같이 확인해 보기" 쪽에 더 가깝다.
Next.js 15 프로젝트를 만들고 Playwright를 붙이다가,
"특정 페이지에서 API 요청이 진짜 잘 나가는지"까지 같이 보고 싶어졌다.
Playwright 공식 문서를 보다가 API 테스트 기능이 있는 걸 발견했고,
그 과정을 기록해 둔 글이다.
- Playwright API test 개념 확인
- baseURL 설정해서 경로만으로 요청 보내기
- 목록 API, 상세 API 테스트 코드 예시
- 테스트하면서 헷갈린 포인트 간단 메모
참고한 문서들은 이쪽이다.
- Playwright API Testing
1. baseURL 설정으로 경로만 써서 이동하기
처음에는 페이지 이동이나 API 요청에 항상 전체 URL을 적고 있었다.
Playwright 문서를 보다 보니 playwright.config.ts의 use.baseURL을 설정해 두면
- page.goto("/")
- request.get("/api/shop")
처럼 경로만 써도 동작하게 만들 수 있다는 걸 뒤늦게 알았다.
설정은 대략 이런 느낌으로 잡았다.
// playwright.config.ts 일부
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: "http://localhost:3000",
/* 필요한 경우 trace 설정 등도 여기에서 같이 관리 */
trace: "on-first-retry",
},

이렇게 해 두면 테스트 코드에서는
- await page.goto("/")
- await request.get("/api/shop")
처럼 훨씬 짧게 쓸 수 있어서 편했다.
2. 목록 API 테스트 – 기본 스키마 확인
먼저 상점 목록을 가져오는 API부터 테스트했다.
목표는 크게 네 가지였다.
- 요청이 200번대 상태 코드로 성공하는지
- 응답이 배열 형태인지
- 배열 길이가 0이 아닌지
- 각 요소가 예상하는 스키마를 가지고 있는지
코드는 이렇게 작성했다.
import { test, expect } from "@playwright/test";
test("상점 목록에서 데이터 확인", async ({ request }) => {
// API 요청 보내기
const getShopData = await request.get("/api/shop");
// 응답 상태 확인 (ok()가 true면 200번대 상태로 간주)
expect(getShopData.ok()).toBeTruthy();
// JSON 데이터 변환
const responseData = await getShopData.json();
// 데이터가 배열인지 확인 (findMany 형태라 배열 예상)
expect(Array.isArray(responseData)).toBe(true);
// 데이터가 비어 있지 않은지 확인
expect(responseData.length).toBeGreaterThan(0);
// 첫 번째 요소의 스키마 검증
expect(responseData[0]).toEqual(
expect.objectContaining({
id: expect.any(String),
title: expect.any(String),
star: expect.any(Number),
description: expect.any(String),
deliveryTime: expect.any(String),
deliveryFee: expect.any(String),
minimumOrder: expect.any(String),
cardImage: expect.any(String),
coverImage: expect.any(String),
tag: expect.any(String),
create_at: expect.any(String),
update_at: expect.any(String),
}),
);
});
여기서 toBeTruthy()를 쓴 이유는
- ok()가 200대 상태 코드를 모두 true로 보기 때문에
- 굳이 toBe(true)까지는 강하게 묶지 않아도 됐기 때문이다.
엄밀하게 true인지 보고 싶다면 toBe(true)로 바꿀 수 있고,
실무에서는 팀 코드 스타일에 맞추면 될 것 같다.
3. 페이지 이동 + 상세 API 테스트 – UUID까지 같이 확인
두 번째로는 상점 목록 페이지에서 마지막 상점 상세 페이지로 이동한 다음,
그 상점의 상세 API와 메뉴 API를 같이 검증하는 흐름을 만들었다.
테스트 흐름은 이렇게 잡았다.
- 홈(/)에서 상점 리스트 페이지(/shop)로 이동
- 마지막 상점 카드의 링크를 찾아서 href를 읽는다
- URL에서 마지막 조각을 잘라서 shopId로 사용한다
- shopId가 UUID 형식인지 정규식으로 검사한다
- 페이지를 실제로 클릭해서 /shop/{shopId}로 이동하는지 확인한다
- 같은 shopId로 두 가지 API를 병렬로 호출한다
- /api/shop?id={shopId}
- /api/shop/{shopId}
- 두 응답 모두 ok인지 확인하고, 각각의 스키마를 검증한다
코드는 다음과 같다.
import { test, expect } from "@playwright/test";
test("상점 상세 이동 및 데이터 확인 - 마지막 상점 (uuid)", async ({
page,
request,
}) => {
await page.goto("/");
// /shop 페이지로 이동하는 링크 클릭
await page.click('a[href*="shop"]');
await expect(page).toHaveURL("/shop");
// 마지막 상점의 링크 선택
const lastShopLink = page.locator('a[href*="/shop/"]').last();
const shopUrl = await lastShopLink.getAttribute("href");
if (!shopUrl) {
throw new Error("Shop URL is not available");
}
// URL에서 shopId 추출
const shopId = shopUrl.split("/").pop();
// UUID 정규식 검사 (검색해서 가져와 사용했다)
const uuidRegex =
/^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$/;
if (!uuidRegex.test(shopId!)) {
throw new Error(`유효하지 않은 포맷: ${shopId}`);
}
// 상점 디테일 페이지로 이동 후 URL 확인
await lastShopLink.click();
await expect(page).toHaveURL(`/shop/${shopId}`);
// 상점과 메뉴 정보를 동시에 요청
const [shopData, menuData] = await Promise.all([
request.get(`/api/shop?id=${shopId}`),
request.get(`/api/shop/${shopId}`),
]);
// 응답 상태 확인
expect(shopData.ok()).toBeTruthy();
expect(menuData.ok()).toBeTruthy();
// JSON 변환
const shop = await shopData.json();
const menu = await menuData.json();
// shop 스키마 검증
expect(shop).toEqual(
expect.objectContaining({
id: expect.any(String),
title: expect.any(String),
star: expect.any(Number),
description: expect.any(String),
deliveryTime: expect.any(String),
deliveryFee: expect.any(String),
minimumOrder: expect.any(String),
cardImage: expect.any(String),
coverImage: expect.any(String),
tag: expect.any(String),
create_at: expect.any(String),
update_at: expect.any(String),
}),
);
// menu 스키마 검증
expect(menu).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: expect.any(String),
category: expect.any(String),
menu: expect.any(String),
price: expect.any(Number),
shopId: expect.any(String),
}),
]),
);
console.log("Shop:", shop);
console.log("Menu:", menu);
});
중간에 UUID 정규식을 가져와서 검사하는 부분은,
"잘못된 URL 구조로 테스트를 통과시키지 말자"는 정도의 안전장치 느낌으로 넣었다.
4. 이번에 새로 알게 된 메서드들 메모
이번 API 테스트를 만들면서, Playwright 쪽에서 특히 인상 깊었던 메서드들을 정리해 둔다.
- request.get()
- Playwright의 APIRequestContext로 HTTP 요청을 보내는 메서드
- response.ok()
- 응답 상태 코드가 200대인지 확인하는 헬퍼
- expect()
- Jest 스타일로 값을 검증하는 어서션 엔트리
- toBeTruthy()
- 값이 truthy인지 확인 (ok()와 같이 쓰기 편했다)
- toBe(true)
- 엄밀하게 true인지 확인
- toBeGreaterThan(n)
- 값이 n보다 큰지 확인 (배열 길이 검증에 사용)
- toEqual()
- 값이 예상한 값과 동일한지 비교
- expect.objectContaining()
- 객체가 특정 속성들을 포함하고 있는지 부분 비교
- expect.arrayContaining()
- 배열이 특정 요소를 포함하고 있는지 비교
- expect.any(Type)
- 값의 타입을 느슨하게 검증할 때 사용
API 스키마가 자주 바뀌는 환경에서는
objectContaining, arrayContaining, any 조합이 꽤 유연하게 느껴졌다.
5. 작게 남겨두는 회고
1편에서 UI를 눌러 보다가, 2편에서는 그 아래 API까지 같이 확인하는 흐름을 만들어 봤다.
이번에 느낀 점을 정리하면 대략 이렇다.
- UI 테스트만으로는 놓치는 부분을 API 테스트가 꽤 잘 메워 준다
- baseURL을 설정해 두니 코드가 훨씬 읽기 좋아졌다
- request 픽스처와 expect.objectContaining 조합이 생각보다 강력했다
다음에 비슷한 구조의 프로젝트에서 API를 검증해야 할 일이 생기면,
이 글의 코드 조각들을 그대로 가져다가 살짝만 고쳐 써도 될 것 같다.
'FrontEnd > Testing' 카테고리의 다른 글
| Next.js 15 + Playwright 도입 – E2E 테스트랑 조금 친해지기 (0) | 2024.11.24 |
|---|
