진짜 API 치는 E2E 테스트는 테스트 시간에 세 가지 참인 것에 의존: API 살아있음, API 빠름, API 가 결정적 데이터 반환. 어느 하나 빠지면 flake. page.route() 가 outbound HTTP 가로채서 테스트에서 응답하게 해, 백엔드 동작이 테스트하려는 게 아닌 케이스에 세 실패 모드 다 제거.
진짜 백엔드 테스트 원할 때 (integration end-to-end, contract 검증) 는 가로채지 마. 두 테스트 모드는 다른 목적 serve 하고 서로 옆에 살아.
Route 로 할 수 있는 네 가지
page.route(pattern, handler) 발동하면 핸들러가 네 exit path 가진 Route 객체 받아:
route.fulfill({ status, body, contentType }) — fake response 로 응답. 요청이 네트워크에 절대 안 닿음.
route.continue({ headers, postData }) — request 수정하고 진짜 서버로 보내.
route.abort() — request 실패. 에러 path 테스트에 유용.
route.fallback() — 다음 매칭 route 핸들러로 미뤄 (route 핸들러 스택).
패턴 매칭
패턴은:
Glob: '**/api/users/*'
Regex: /\/api\/users\/\d+/
URL 에 대한 predicate 함수.
Glob 이 쉬운 default. 더 엄격한 매칭 필요할 때 (예: 숫자 id 만) regex. URL 이상에 의존하는 매칭 (method, header) 엔 predicate.
Scope — page vs context vs Browser
Scope 세 레벨:
page.route() — 이 Page 의 요청만 가로채. 좋은 default.
context.route() — context 의 모든 Page 에서 가로채 (드물; 공유 state 브라우저 컨텍스트에 유용).
browser.route() — 존재 안 함; 대신 fixture 로 올려.
test.beforeEach 에서 추가한 route 는 테스트 동안 지속; fixture setup 에서 추가한 route 는 fixture 만큼 지속.
테스트가 UI 에 대한 거면 네트워크 mock; 테스트가 네트워크에 대한 거면 mock 하지 마. 'API 가 세 아이템 반환하면 카트가 세 아이템 보여줌' 단언하는 테스트는 UI 테스트 — mock. '결제 흐름이 staging 에 대해 완료됨' 단언하는 테스트는 contract 테스트 — mock 하지 마.
HAR 파일 — 한 번 녹화, 영원히 replay
수동 mocking 이 지루한 복잡한 흐름엔 Playwright 가 진짜 백엔드에 대해 한 번 HAR 파일 (모든 request/response 의 archive) 녹화하고, 후속 모든 run 에 replay 가능. 결과: per-request mock 코드 zero 인 현실적 response.
백엔드가 복잡하지만 안정적일 때 HAR 패턴 써; 변형 response (에러, edge case) 테스트해야 할 때 수동 route.fulfill 로 돌아와.
Code
테스트 둘, 같은 endpoint, 다른 response·typescript
// Fulfilling a request — the most common pattern
import { test, expect } from '@playwright/test';
test('shows the user list when API returns three', async ({ page }) => {
await page.route('**/api/users', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([
{ id: 1, name: 'Pippa', email: 'pippa@example.com' },
{ id: 2, name: 'Dad', email: 'dad@example.com' },
{ id: 3, name: 'Mom', email: 'mom@example.com' },
]),
});
});
await page.goto('/users');
await expect(page.getByRole('listitem')).toHaveCount(3);
await expect(page.getByText('Pippa')).toBeVisible();
});
test('shows the empty state when API returns nothing', async ({ page }) => {
await page.route('**/api/users', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: '[]',
});
});
await page.goto('/users');
await expect(page.getByText(/no users yet/i)).toBeVisible();
});
// Modifying a real request — continue() with overrides
import { test } from '@playwright/test';
test('injects an x-test header on outbound requests', async ({ page }) => {
await page.route('**/api/**', async (route, request) => {
const headers = {
...request.headers(),
'x-test-run': 'pippa-quest-suite',
};
await route.continue({ headers });
});
await page.goto('/');
// The real API receives every request with the extra header attached.
// Useful for tagging test traffic in observability.
});
HAR 패턴 — 한 번 녹화, 항상 replay·typescript
// HAR record/replay — when manual mocking would be tedious
import { test, expect } from '@playwright/test';
// First run — record
test('records the user-list flow', async ({ page, context }) => {
test.skip(!process.env.RECORD, 'Only runs with RECORD=true');
await context.routeFromHAR('e2e/.har/users-flow.har', {
update: true, // record this run into the HAR
url: '**/api/**',
});
await page.goto('/users');
// ... run the flow once against the real backend
});
// Every subsequent run — replay
test('replays the user-list flow from HAR', async ({ page, context }) => {
await context.routeFromHAR('e2e/.har/users-flow.har', {
url: '**/api/**',
});
await page.goto('/users');
await expect(page.getByRole('listitem')).toHaveCount(3);
});
Suite 에서 진짜 백엔드 치는 E2E 테스트 골라. 그 테스트의 메인 API endpoint 를 mock 하는 page.route() 추가. 진짜 백엔드 멈췄을 때 (dev 서버 일시적으로 죽임) 도 테스트 통과 확인. 그 다음 같은 컴포넌트에 대한 두 번째 테스트 짜는데 API mock 이 500 반환 — 에러 UI 가 맞게 렌더되는지 확인. 두 번째 테스트가 진짜 백엔드에 대해 짜기 얼마나 어려웠을지 주목.
Hint
Route 추가 후에도 테스트가 네트워크 치면 패턴이 매칭 안 하는 거. Playwright 의 --debug 모드로 어느 요청이 날고 있는지 그리고 URL 이 뭔지 봐, 그 다음 정확한 path 매칭하게 glob 좁혀.
Progress
Progress is local-only — sign in to sync across devices.