E2E 테스트
E2E(End-to-End) 테스트는 실제 브라우저에서 사용자의 행동을 시뮬레이션하여 애플리케이션 전체가 올바르게 동작하는지 검증합니다.
면접에서 "단위 테스트와 E2E 테스트의 차이"를 물어보면, 테스트 피라미드 에서의 역할과 비용을 설명할 수 있어야 합니다.
테스트 피라미드
┌─────────┐
│ E2E │ ← 적게, 핵심 시나리오만
─┼─────────┼─
│ 통합 테스트 │
─┼─────────────┼─
│ 단위 테스트 │ ← 많이, 빠르게
─┴─────────────────┴─
E2E: 느리고 비용이 높지만, 실제 사용자 경험을 검증
단위: 빠르고 비용이 낮지만, 통합 동작을 보장하지 않음
Playwright 설정
// playwright.config.ts
import { defineConfig } from '@playwright/test'
export default defineConfig({
testDir: './e2e',
timeout: 30000,
use: {
baseURL: 'http://localhost:5173',
trace: 'on-first-retry',
screenshot: 'only-on-failure'
},
webServer: {
command: 'npm run dev',
port: 5173,
reuseExistingServer: !process.env.CI
}
})
Playwright 테스트 작성
// e2e/login.spec.ts
import { test, expect } from '@playwright/test'
test.describe('로그인 플로우', () => {
test('유효한 자격 증명으로 로그인 성공', async ({ page }) => {
await page.goto('/login')
// 폼 입력
await page.fill('[data-testid="email"]', 'test@example.com')
await page.fill('[data-testid="password"]', 'password123')
await page.click('[data-testid="submit"]')
// 대시보드로 리다이렉트 확인
await expect(page).toHaveURL('/dashboard')
await expect(page.locator('[data-testid="welcome"]')).toContainText('안녕하세요')
})
test('잘못된 비밀번호로 에러 메시지 표시', async ({ page }) => {
await page.goto('/login')
await page.fill('[data-testid="email"]', 'test@example.com')
await page.fill('[data-testid="password"]', 'wrong')
await page.click('[data-testid="submit"]')
await expect(page.locator('[data-testid="error"]')).toBeVisible()
await expect(page.locator('[data-testid="error"]')).toContainText('비밀번호가 올바르지 않습니다')
})
})
Cypress 테스트 작성
// cypress/e2e/todo.cy.ts
describe('Todo 앱', () => {
beforeEach(() => {
cy.visit('/')
})
it('새 할일을 추가할 수 있다', () => {
cy.get('[data-testid="new-todo"]').type('Vue 공부하기{enter}')
cy.get('[data-testid="todo-list"]')
.should('contain', 'Vue 공부하기')
})
it('할일을 완료 처리할 수 있다', () => {
cy.get('[data-testid="new-todo"]').type('테스트 작성{enter}')
cy.get('[data-testid="todo-checkbox"]').first().click()
cy.get('[data-testid="todo-item"]').first()
.should('have.class', 'completed')
})
it('완료된 할일을 삭제할 수 있다', () => {
cy.get('[data-testid="new-todo"]').type('삭제할 항목{enter}')
cy.get('[data-testid="delete-btn"]').first().click()
cy.get('[data-testid="todo-list"]')
.should('not.contain', '삭제할 항목')
})
})
Page Object Model 패턴
// e2e/pages/LoginPage.ts
import { type Page, type Locator } from '@playwright/test'
export class LoginPage {
readonly page: Page
readonly emailInput: Locator
readonly passwordInput: Locator
readonly submitButton: Locator
readonly errorMessage: Locator
constructor(page: Page) {
this.page = page
this.emailInput = page.locator('[data-testid="email"]')
this.passwordInput = page.locator('[data-testid="password"]')
this.submitButton = page.locator('[data-testid="submit"]')
this.errorMessage = page.locator('[data-testid="error"]')
}
async goto() {
await this.page.goto('/login')
}
async login(email: string, password: string) {
await this.emailInput.fill(email)
await this.passwordInput.fill(password)
await this.submitButton.click()
}
}
// e2e/login.spec.ts
import { test, expect } from '@playwright/test'
import { LoginPage } from './pages/LoginPage'
test('로그인 성공', async ({ page }) => {
const loginPage = new LoginPage(page)
await loginPage.goto()
await loginPage.login('test@example.com', 'password123')
await expect(page).toHaveURL('/dashboard')
})
어떤 것을 E2E로 테스트해야 하나?
E2E 테스트 대상:
✓ 핵심 사용자 플로우 (로그인, 결제, 가입)
✓ 여러 페이지에 걸친 시나리오
✓ 브라우저 특유 동작 (쿠키, 로컬스토리지)
단위/컴포넌트 테스트 대상:
✓ 개별 컴포넌트 동작
✓ Composable 로직
✓ 유틸리티 함수
✓ 에지 케이스
면접 팁
- E2E 테스트는 비용이 높으므로 핵심 시나리오만 작성하는 것이 원칙입니다
- Playwright vs Cypress 선택 기준: Playwright는 다중 브라우저 지원과 병렬 실행이 강점, Cypress는 개발자 경험과 디버깅이 강점
- Page Object Model 패턴 을 사용하면 테스트 유지보수성이 크게 향상됩니다
요약
E2E 테스트는 실제 브라우저에서 사용자 시나리오를 검증합니다. Playwright이나 Cypress를 활용하며, 핵심 플로우만 테스트하고 나머지는 단위/컴포넌트 테스트로 커버합니다. Page Object Model 패턴으로 테스트 코드의 유지보수성을 높이세요.
댓글 로딩 중...