본문으로 건너뛰기
Advertisement

16.5 E2E 테스팅 — Playwright + TypeScript

Playwright 설치 및 설정

npm install --save-dev @playwright/test
npx playwright install # 브라우저 설치

playwright.config.ts

import { defineConfig, devices } from '@playwright/test'

export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: [['html'], ['list']],

use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},

projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'mobile', use: { ...devices['iPhone 14'] } },
],

webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
})

기본 테스트 작성

// e2e/auth.spec.ts
import { test, expect } from '@playwright/test'

test.describe('인증 플로우', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/')
})

test('로그인 성공', async ({ page }) => {
await page.goto('/login')

await page.getByLabel('이메일').fill('alice@example.com')
await page.getByLabel('비밀번호').fill('password123')
await page.getByRole('button', { name: '로그인' }).click()

// 로그인 후 대시보드로 이동
await expect(page).toHaveURL('/dashboard')
await expect(page.getByText('환영합니다, Alice!')).toBeVisible()
})

test('잘못된 비밀번호로 로그인 실패', async ({ page }) => {
await page.goto('/login')

await page.getByLabel('이메일').fill('alice@example.com')
await page.getByLabel('비밀번호').fill('wrongpassword')
await page.getByRole('button', { name: '로그인' }).click()

await expect(page.getByText('이메일 또는 비밀번호가 올바르지 않습니다.')).toBeVisible()
await expect(page).toHaveURL('/login')
})

test('인증되지 않은 사용자 리디렉션', async ({ page }) => {
await page.goto('/dashboard')
await expect(page).toHaveURL('/login')
})
})

페이지 오브젝트 패턴 (POM)

// e2e/pages/login.page.ts
import { Page, Locator, expect } 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.getByLabel('이메일')
this.passwordInput = page.getByLabel('비밀번호')
this.submitButton = page.getByRole('button', { name: '로그인' })
this.errorMessage = page.getByTestId('error-message')
}

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()
}

async expectErrorMessage(message: string) {
await expect(this.errorMessage).toContainText(message)
}
}

// e2e/pages/dashboard.page.ts
export class DashboardPage {
readonly page: Page
readonly welcomeMessage: Locator
readonly userMenu: Locator

constructor(page: Page) {
this.page = page
this.welcomeMessage = page.getByTestId('welcome-message')
this.userMenu = page.getByRole('button', { name: '사용자 메뉴' })
}

async expectLoaded(userName: string) {
await expect(this.page).toHaveURL('/dashboard')
await expect(this.welcomeMessage).toContainText(userName)
}

async logout() {
await this.userMenu.click()
await this.page.getByRole('menuitem', { name: '로그아웃' }).click()
}
}
// e2e/auth.spec.ts — POM 사용
import { test, expect } from '@playwright/test'
import { LoginPage } from './pages/login.page'
import { DashboardPage } from './pages/dashboard.page'

test.describe('인증 (POM 패턴)', () => {
test('로그인 → 대시보드 → 로그아웃', async ({ page }) => {
const loginPage = new LoginPage(page)
const dashboardPage = new DashboardPage(page)

await loginPage.goto()
await loginPage.login('alice@example.com', 'password123')
await dashboardPage.expectLoaded('Alice')
await dashboardPage.logout()

await expect(page).toHaveURL('/login')
})
})

타입 안전한 Fixtures

// e2e/fixtures.ts
import { test as base, expect } from '@playwright/test'
import { LoginPage } from './pages/login.page'
import { DashboardPage } from './pages/dashboard.page'

// Fixture 타입 정의
type MyFixtures = {
loginPage: LoginPage
dashboardPage: DashboardPage
authenticatedPage: { page: typeof base.page }
}

// Fixture 확장
export const test = base.extend<MyFixtures>({
loginPage: async ({ page }, use) => {
await use(new LoginPage(page))
},

dashboardPage: async ({ page }, use) => {
await use(new DashboardPage(page))
},

// 미리 로그인된 페이지 fixture
authenticatedPage: async ({ page }, use) => {
await page.goto('/login')
await page.getByLabel('이메일').fill('alice@example.com')
await page.getByLabel('비밀번호').fill('password123')
await page.getByRole('button', { name: '로그인' }).click()
await expect(page).toHaveURL('/dashboard')
await use({ page })
},
})

export { expect }
// e2e/dashboard.spec.ts — 커스텀 fixture 사용
import { test, expect } from './fixtures'

test('로그인된 상태에서 프로필 접근', async ({ authenticatedPage }) => {
const { page } = authenticatedPage
await page.goto('/profile')
await expect(page.getByText('내 프로필')).toBeVisible()
})

API Mocking

// e2e/api-mock.spec.ts
import { test, expect } from '@playwright/test'

test('API 응답 모킹', async ({ page }) => {
// 실제 API 대신 mock 응답 사용
await page.route('**/api/users', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
data: [
{ id: '1', name: 'Alice', email: 'alice@example.com' },
{ id: '2', name: 'Bob', email: 'bob@example.com' },
],
total: 2,
}),
})
})

await page.goto('/users')
await expect(page.getByText('Alice')).toBeVisible()
await expect(page.getByText('Bob')).toBeVisible()
await expect(page.getByTestId('user-count')).toHaveText('2명')
})

test('API 에러 처리 테스트', async ({ page }) => {
await page.route('**/api/users/**', async (route) => {
await route.fulfill({
status: 404,
contentType: 'application/json',
body: JSON.stringify({ error: { message: '사용자를 찾을 수 없습니다.' } }),
})
})

await page.goto('/users/999')
await expect(page.getByText('사용자를 찾을 수 없습니다.')).toBeVisible()
})

고수 팁

비주얼 회귀 테스트

test('스냅샷 비교', async ({ page }) => {
await page.goto('/components/button')

// 스크린샷 스냅샷
await expect(page).toHaveScreenshot('button-default.png')
await expect(page.getByRole('button')).toHaveScreenshot('button-element.png')
})

CI 환경 최적화

// playwright.config.ts
export default defineConfig({
// CI에서 느린 테스트 타임아웃 증가
timeout: process.env.CI ? 60_000 : 30_000,

expect: {
// 비주얼 테스트 임계값
toHaveScreenshot: {
maxDiffPixels: 100,
},
},
})
Advertisement