Playwright 테스트 확장: 페이지 객체에서 의존성 주입 및 데이터 격리까지
동적 렌더링을 사용하는 SPA에서 페이지 객체 생성자는 비동기성으로 인해 위험을 초래합니다. 생성자에서 IIFE를 통해 상태를 초기화하려고 하면 경쟁 조건이 발생합니다: 테스트가 데이터가 준비되기 전에 접근하게 되어, CI에서 병렬 실행 시 문제가 드러납니다.
// 잘못된 예: 생성자에서 상태 초기화
constructor(page: Page) {
(async () => {
this.initialCount = await page.locator('.items').count();
})();
}
표준 동기 로케이터 초기화도 최적이 아닙니다—객체가 생성 시점의 DOM 스냅샷을 캡처합니다. 게터를 사용하면 늦은 바인딩으로 문제를 해결할 수 있습니다:
// 올바른 예: 현재 DOM을 위한 게터
get submitButton() {
return this.page.getByRole('button', { name: '주문하기' });
}
이렇게 하면 상태 없는 동작이 보장됩니다: 각 호출은 초기 상태에 묶이지 않고 현재 DOM을 확인합니다.
Playwright Fixture를 통한 의존성 주입
테스트에서 직접 객체 생성(new LoginPage(page))은 리팩토링을 복잡하게 만듭니다: 생성자를 변경하려면 프로젝트 전체를 수정해야 합니다. Fixture는 인스턴스 캐싱이 있는 DI 컨테이너 역할을 합니다.
export const test = base.extend({
cartPage: async ({ page }, use) => {
await use(new CartPage(page));
},
checkoutFlow: async ({ cartPage, checkoutPage }, use) => {
await use(new CheckoutFlow(cartPage, checkoutPage));
}
});
test('주문하기', async ({ authFlow, checkoutFlow }) => {
await authFlow.loginAs(user);
await checkoutFlow.submitOrder();
});
테스트는 구현과 독립적으로 명세에 집중할 수 있습니다.
Fixture 확장: mergeTests와 네임스페이싱
20개 이상의 Fixture가 있으면 단일 파일은 관리하기 어려워집니다. 도메인별로 분리하고 mergeTests를 사용하세요:
// auth.fixtures.ts
export const authTest = base.extend({ ... });
// fixtures.ts
import { mergeTests } from '@playwright/test';
export const test = mergeTests(authTest, cartTest);
네임스페이싱으로 이름 충돌을 피하세요:
export const test = base.extend({
auth: async ({ page }, use) => {
await use({ admin: new Admin(page), user: new User(page) });
}
});
test('관리자 로그인 가능', async ({ auth }) => {
await auth.admin.loginAs();
});
병렬 실행에서 데이터 격리
workerIndex는 샤딩이 있는 CI에서는 부족합니다. 조합을 사용하세요:
- CI에서 가져온 RUN_ID (예: GITHUB_RUN_ID)
- 테스트 경로의 해시인 testId
- 재시도를 위한 repeatEachIndex
이 복합 키로 Faker를 시드하면 재현성을 보장합니다:
export function seedFaker(testInfo: TestInfo) {
const RUN_ID = process.env.RUN_ID || 'local';
const seed = hashCode(`${testInfo.testId}-${RUN_ID}`);
faker.seed(seed);
return faker;
}
export const test = base.extend({
faker: async ({}, use, testInfo) => {
await use(seedFaker(testInfo));
}
});
CI-RUN_ID를 사용한 로컬 실행은 실패 시 정확한 데이터를 재현합니다.
데이터 계층: 팩토리, 오버라이드, 데이터셋
- Faker: 중요하지 않은 필드(이름, 이메일)에 노이즈 추가
- 팩토리: 기본값으로 구조 생성
- 오버라이드: 테스트에서 중요한 변경
- 데이터셋: 반복되는 비즈니스 케이스
export function createUser(overrides?: Partial<User>, f = faker): User {
return {
id: f.string.uuid(),
name: f.person.fullName(),
role: '고객',
...overrides
};
}
export const VIP_USER = { role: 'vip', discount: 0.15 };
테스트: const vipUser = createUser({ ...VIP_USER }, faker);
단계 의미론과 ESLint 가드
기술적 클릭과 비즈니스 로직을 @Step으로 분리하세요:
@Step('주문 #${orderId} 처리 중')
async submitOrder(orderId: string) {
await this.checkoutPage.fillDetails(orderId);
await this.checkoutPage.submit();
}
비즈니스 단계('로그인')는 기술적 단계('로그인 버튼 클릭')보다 UI 변경에 더 강인합니다.
ESLint는 직접 new PageObject를 금지합니다:
{
"no-restricted-syntax": ["error", {
"selector": "NewExpression[callee.name=/.*Page$/]",
"message": "new 대신 Fixture 사용"
}]
}
핵심 요약
- POM에서 게터를 사용하면 상태 함정 없이 현재 DOM 보장
- Fixture를 DI로 사용하면 1000개 이상의 테스트에서 리팩토링 최소화
- RUN_ID + testId로 Faker 시드하여 CI 샤딩에서 안정성 확보
- 팩토리 + 데이터셋으로 중복 코드 없이 데이터 격리
- 네임스페이싱 Fixture로 mergeTests에서 충돌 방지
— Editorial Team
아직 댓글이 없습니다.