모든 Playwright 테스트가 첫 인자에서 fixture destructure: test('thing', async ({ page, context, request }) => {...}). page, context, request 가 내장 fixture. Playwright 가 테스트당 fresh page (그리고 default 로 context) 생성, 그래서 격리 자동.
덜 명확한 건 fixture 세트 확장 가능하다는 거. 커스텀 fixture 는 테스트에 값 yield 하는 작은 setup-teardown 로직 조각. 값은 뭐든 — 로그인된 페이지, 시드된 DB 연결, feature-flag overrider.
`test.extend` 패턴
커스텀 fixture 생성엔 test.extend({ fixtureName: async ({ deps }, use) => { ... } }) 호출. Fixture 함수 안에서 값 setup, await use(value) 호출해서 테스트에 넘기고, 테스트 끝난 후 teardown 실행. 모양이 React 의 `useEffect` cleanup 패턴과 동일 — setup, yield, cleanup.
반환된 test 객체가 내장 fixture AND 너의 것 둘 다 가진 NEW 테스트 runner. @playwright/test 대신 이걸 테스트 파일에서 import.
테스트 scope vs Worker scope fixture
Fixture 는 SCOPE 가짐 — 얼마나 자주 setup 되고 tear down 되는지:
test scope (default) — 매 테스트 전 돌고, 후 tear down. 테스트별 fresh state 필요한 거에 (로그인된 페이지, 깨끗한 DB).
worker scope — worker 프로세스당 한 번 돌아. 공유 가능한 비싼 setup 에 (DB 연결, API 클라이언트). Fixture 가 그 worker 의 모든 테스트에 재사용.
재사용이 안전할 때만 worker scope 선택. 공유 DB 연결은 괜찮; 공유 '로그인된 사용자' state 는 아냐 — 같은 세션에 쓰는 여러 병렬 테스트는 버그.
Fixture 는 테스트에 결합된 반복 setup 을 숨길 때 값어치해. 모든 테스트가 await ensureLoggedIn(page); await seedThreeUsers(); 로 시작하면 둘 다 seededLoggedInPage fixture 로 접고 그냥 test('...', async ({ seededLoggedInPage }) => {...}) 짜. 의도 표면화; 의식 사라짐.
병렬 실행 모델
Playwright 가 default 로 테스트를 병렬로 돌려. 병렬성 두 차원:
파일 사이 — 여러 테스트 파일이 별개 worker 프로세스에서 동시 실행. workers config 옵션이 제어 (default: CPU 코어의 절반).
파일 안 — 파일 상단에 test.describe.configure({ mode: 'parallel' }) 설정 (또는 config 에 fullyParallel: true) 하면 같은 파일 안 테스트도 worker 사이 병렬 실행.
Default 동작은 보수적: 파일 사이 병렬, 안에선 직렬. 테스트가 잘 격리됐으면 전역 fullyParallel: true 설정; 한 파일 안 시퀀스가 엄격히 순서 필요하면 파일별로 mode: 'serial' 로 꺼.
격리 계약
병렬 테스트는 격리 필요. Playwright 가 브라우저 쪽 격리 (별개 컨텍스트) 줘, 근데 서버 / DB 쪽은 너의 책임. 전략:
테스트별 DB row scope (모든 테스트가 unique 데이터 씀).
Worker 별 DB (각 worker 가 자기 DB 에 연결).
테스트별 transaction rollback (각 테스트가 끝에 rollback 되는 transaction 안에서 돎).
서버 쪽 격리 없으면 병렬성이 1 에 cap — Playwright 의 속도 대부분 버린 거.
Code
커스텀 fixture — 시드되고 로그인된 대시보드·typescript
// e2e/fixtures.ts — define custom fixtures
import { test as base, expect, type Page } from '@playwright/test';
type MyFixtures = {
/** A page that's already navigated to /dashboard with three users seeded. */
seededDashboard: Page;
};
export const test = base.extend<MyFixtures>({
seededDashboard: async ({ page, request }, use) => {
// Setup — seed the data and navigate
await request.post('/api/test/reset');
await request.post('/api/test/seed', {
data: { users: [{ name: 'Pippa' }, { name: 'Dad' }, { name: 'Mom' }] },
});
await page.goto('/dashboard');
await expect(page.getByRole('heading', { name: /dashboard/i })).toBeVisible();
// Yield the configured page to the test
await use(page);
// Teardown — clean up after
await request.post('/api/test/reset');
},
});
export { expect };
커스텀 fixture 쓰는 테스트·typescript
// e2e/dashboard.spec.ts — use the custom fixture
import { test, expect } from './fixtures';
test('renders all three seeded users', async ({ seededDashboard }) => {
// No setup needed in the test — the fixture handled it
await expect(seededDashboard.getByRole('listitem')).toHaveCount(3);
await expect(seededDashboard.getByText('Pippa')).toBeVisible();
});
test('clicking a user opens their detail page', async ({ seededDashboard }) => {
await seededDashboard.getByText('Dad').click();
await expect(seededDashboard).toHaveURL(/\/users\/\d+/);
});
Worker scope fixture — worker 안 테스트들 사이 공유·typescript
// Worker-scoped fixture — set up once per worker process
import { test as base } from '@playwright/test';
type WorkerFixtures = {
/** Shared API client — created once per worker, reused across tests in that worker. */
apiClient: { post: (url: string, body: unknown) => Promise<Response> };
};
export const test = base.extend<{}, WorkerFixtures>({
apiClient: [
async ({}, use) => {
// Setup: create the client once
const client = await createApiClient({
baseUrl: process.env.API_URL!,
token: process.env.SERVICE_TOKEN!,
});
// Yield to all tests in this worker
await use(client);
// Teardown: close the client when the worker exits
await client.close();
},
{ scope: 'worker' }, // ← the magic that changes scope
],
});
파일 안 병렬 vs 직렬·typescript
// Parallel mode inside a file
import { test, expect } from '@playwright/test';
// All tests in this file run in parallel across workers
test.describe.configure({ mode: 'parallel' });
test('test A', async ({ page }) => { /* ... */ });
test('test B', async ({ page }) => { /* ... */ });
test('test C', async ({ page }) => { /* ... */ });
// Or force serial when a file's tests depend on each other (rare and a smell)
test.describe.configure({ mode: 'serial' });
test('step 1 — create user', async ({ page }) => { /* ... */ });
test('step 2 — use the user from step 1', async ({ page }) => { /* ... */ });
// (Better: collapse 'step 1' + 'step 2' into one test that does both)
최소 세 E2E 테스트에 걸쳐 반복되는 setup 블록 (로그인, 데이터 seed, 시작 페이지로 navigate) 찾아. 커스텀 fixture 로 추출. 세 테스트를 destructuring 통해 쓰도록 업데이트. 테스트 여전히 통과하고 테스트 본문 더 짧고 가독적인지 확인. 보너스: 비싼 setup 하나를 worker scope fixture 로 변환하고 차이 시간 측정.
Hint
커스텀 fixture 가 setup 중 에러 throw 하면 테스트가 혼란스러운 'fixture setup failed' 메시지로 실패. 위험한 setup 을 try/catch 로 wrap 하고 더 명확한 에러로 재 throw — fixture 작성자는 소비자에게 좋은 진단 제공 기대됨.
Progress
Progress is local-only — sign in to sync across devices.