Convention
E2E 테스트 규칙
올라핀테크 프론트엔드 프로젝트 내에서 지켜야하는 E2E 테스트 규칙을 서술합니다.
E2E 테스팅
1. 디렉터리 구조
e2e/
├── core/ # 테스트 인프라
│ ├── api-mock-handlers/ # API Mock 시스템
│ │ ├── index.ts # Mock 핸들러 메인
│ │ └── mocks/ # 도메인별 Mock 데이터
│ │ ├── contract.ts
│ │ ├── factoring.ts
│ │ ├── product-margin.ts
│ │ ├── remote-config.ts
│ │ └── // ...
│ ├── sequences/ # 재사용 가능한 시퀀스
│ │ ├── user-login-success-sequence.ts
│ │ └── user-login-failed-sequence.ts
│ └── utils/ # 유틸리티
│ ├── go-to-page.ts
│ └── url-matchers.ts
│
└── tests/ # 테스트 케이스
├── auth/
│ ├── sign-in/
│ │ └── flow/
│ │ └── sign-in.flow.spec.ts
│ └── // ...
├── contract/
│ └── flow/
│ └── contract-flow.spec.ts
└── factoring/
├── flow/
└── ui/2. 테스트 작성 패턴
플로우 함수로 구조화
test.describe('계약 플로우', () => {
/**
* @시나리오
* - 로그인 후 마이페이지로 이동
* - 계약이 필요한 상태
* @조건
* - 로그인 상태여야 함
* - 계약이 필요한 상태여야 함
* - 조회 성공 응답이 있어야 함
* @검증
* - 단계 별 인증을 검증함
* - 최종 계약 완료 확인
*/
test('성공 플로우', async ({ page, next }) => {
apiMockHandlers(next, [
{
match: signatureMatch(API_SIGNATURE.CONTRACT.CHECK_CONDITION),
responseBody: CHECK_CONTRACT_CONDITION_RESPONSE_FIXTURE.SUCCESS.data,
},
{
match: signatureMatch(API_SIGNATURE.MY_CONTRACT.INFO),
responseBody:
GET_CONTRACT_INFO_FIXTURE.SUCCESS_보증보험동의계약필요.data,
},
])
await 유저_로그인_성공_플로우(page)
await 계약_조회_플로우(page)
await 계약_인증_플로우(page)
await 계약_서명_플로우(page)
await 계약_완료_플로우(page)
})
})플로우 함수 패턴 장점:
- 테스트 단계별로 명확한 구조
- 재사용 가능
- 한글 함수명으로 가독성 향상
- 유지보수 용이
3. API Mock 시스템
Mock 사용 예시
- 모든 happy path가 갖춰진
apiMockHandlers를 사용하고, 필요한 API를 테스트별로 오버라이딩 하여 사용
test('로그인 성공', async ({ page, next }) => {
apiMockHandlers(next, [
{
match: signatureMatch(API_SIGNATURE.AUTH.SIGN_IN),
responseBody: SIGN_IN_FIXTURE.SUCCESS.data,
},
])
await ensureCredentials(page)
await 유저_로그인_성공_플로우(page)
})
test('서버 오류로 로그인 실패', async ({ page, next }) => {
apiMockHandlers(next, [
{
match: signatureMatch(API_SIGNATURE.AUTH.SIGN_IN),
responseBody: SIGN_IN_FIXTURE.SERVER_ERROR,
serverError: true, // 500 에러 시뮬레이션
},
])
await ensureCredentials(page)
await 유저_로그인_성공_플로우(page)
})4. 재사용 가능한 Sequence
- 로그인 등 일반적인 플로우는 재사용 가능한 플로우 함수로 구현
export const 유저_로그인_성공_플로우 = async (page: Page) => {
await goToPage(page, Routes.signIn())
const idInput = page.getByPlaceholder('-제외 10자리 입력')
const passwordInput = page.getByPlaceholder('비밀번호를 입력해주세요')
await expect(idInput).toBeEditable()
await expect(passwordInput).toBeEditable()
// ...
}5. 선택자 베스트 프랙티스
// ✅ 1순위: Test ID
page.getByTestId('user-info-menu')
// ✅ 2순위: Role 기반
page.getByRole('button', { name: '로그인' })
page.getByRole('textbox', { name: '비밀번호' })
// ⚠️ 3순위: Label
page.getByLabel('짝짝짝 👏🏻계약이 완료 되었어요!')
// ⚠️ 4순위: Text (변경 가능성)
page.locator('text="로그인 정보가 일치하지 않아요"')
// ❌ 지양: CSS/XPath (취약)
page.locator('.login-button')6. 대기 패턴
// ✅ 명시적 대기
await page.getByTestId('user-info-menu').waitFor({ state: 'visible' })
// ✅ expect 타임아웃
await expect(page.locator('text="로그인 성공"')).toBeVisible({
timeout: 50000,
})
// ✅ URL 대기
await page.waitForURL(Routes.contractInfo())
// ❌ 암시적 대기 (지양)
await page.waitForTimeout(5000)
// ❌ 네트워크 상태 대기 (지양)
await page.waitForLoadState('networkidle')
await page.waitForLoadState('domcontentloaded')
await page.waitForLoadState('load')