C.W.K.
Stream
Lesson 03 of 04 · published

커스텀 Fixture + 병렬 실행

~17 min · playwright-advanced, fixtures, parallel

Level 0테스트 호기심
0 XP0/32 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete
"커스텀 fixture 는 50번 안 써도 되는 보일러플레이트."

Fixture 가 이미 주는 것

모든 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)

External links

Exercise

최소 세 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.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.