๐งช Next.js ์ฌํ 9์ฅ: Testing Strategy โ Jest + MSW + Playwright ์ค์ ํ ์คํธ
๐ ๊ฐ์
Jest + MSW + Playwright๋ก Next.js ์ฑ์ ๋จ์ยทํตํฉยทE2E ํ ์คํธํ๋ ์ ๋ต์ ์ ๋ฆฌํฉ๋๋ค.
๐ ๋ชฉ์ฐจ
- ๐ ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
- ๐ค ์ ์์์ผ ํ๋๊ฐ
- ๐๏ธ ๋น์ ๋ก ๋จผ์ ์ดํดํ๊ธฐ
- ๐งฉ ํ ์คํธ ํผ๋ผ๋ฏธ๋ โ ์ด๋ค ํ ์คํธ๋ฅผ ์ผ๋ง๋ ๐ข
- โ๏ธ ํ๊ฒฝ ์ค์ โ Jest + Testing Library ๐ข
- ๐ฌ ๋จ์ยทํตํฉ ํ ์คํธ โ ์ปดํฌ๋ํธ์ Server Action ๐ก
- ๐ MSW๋ก API ๋ชจํน ๐ก
- ๐ญ Playwright E2E ํ ์คํธ ๐ด
- ๐ฅ ์๋ฌ ํด๊ฒฐ ์นดํ๋ก๊ทธ
- ๐ ์ด๋ฒ์ ๋ฐฐ์ด ๋ด์ฉ ์ด์ ๋ฆฌ
- ๐ ๋ง๋ฌด๋ฆฌ ํด์ฆ
- ๐ ๋ ์์๋ณด๊ธฐ
๐ ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
โฑ๏ธ ์์ ์ฝ๊ธฐ ์๊ฐ: 20๋ถ (์ ์ฒด) / ํต์ฌ ํํธ๋ง: 10๋ถ
๐บ๏ธ ์ด ๋ฌธ์์ ๋ฐฐ๊ฒฝ ์ธ๊ณ๊ด: '์์๋ค ์ปค๋ฎค๋ํฐ'
- ์์ฒ (์ ์ ): "์ฝ๋ ์ง๊ณ ๋์ ์ ๊ธฐ๋ฅ ์ถ๊ฐํ ๋๋ง๋ค ์ด์ ๊ธฐ๋ฅ์ด ๊นจ์ง๋์ง ์์ผ๋ก ๋ค ํด๋ฆญํด์ ํ์ธํ๊ณ ์์ด์. ๊ธฐ๋ฅ์ด ๋์ด๋ ์๋ก ํ์ธํด์ผ ํ ๊ฒ ๋๋ฌด ๋ง์์ ๋น ๋จ๋ฆฌ๋ ๊ฒ๋ ์๊ธฐ๊ณ , ๋ฐฐํฌํ ๋๋ง๋ค ๋๋ ค์์."
- ์ํธ(๋ฆฌ๋): "์์ฒ ๋, ๊ทธ๊ฒ ๋ฐ๋ก ํ ์คํธ ์ฝ๋๊ฐ ํ์ํ ์ด์ ์์. '๊ธฐ์กด ๊ธฐ๋ฅ์ด ์ ์ฝ๋์๋ ์ฌ์ ํ ์ ๋์ํ๋๊ฐ'๋ฅผ ์๋์ผ๋ก ๊ฒ์ฆํด์ฃผ๋ ๊ฒ ํ ์คํธ์์. ์ฒ์ ์ง๋ ์๊ฐ๋ณด๋ค ๋์ค์ ๋ฒ๊ทธ ์ก๋ ์๊ฐ์ด ํจ์ฌ ๋ ๊ธธ์ด์. ํ ์คํธ๊ฐ ์์ผ๋ฉด ๋ฆฌํฉํ ๋ง๋, ์ ๊ธฐ๋ฅ ์ถ๊ฐ๋ ๋๋ ต์ง ์์์."
๐บ๏ธ ์ด ๋ฌธ์์ ํ๋ฆ
ํ
์คํธ ํผ๋ผ๋ฏธ๋ ์ ๋ต โ ํ๊ฒฝ ์ค์ โ ์ปดํฌ๋ํธ ํ
์คํธ โ Server Action ํ
์คํธ โ MSW API ๋ชจํน โ E2E ํ
์คํธ
๐ฏ ์ด ๋ฌธ์๋ฅผ ๋ค ์ฝ์ผ๋ฉด ํ ์ ์๋ ๊ฒ
- Jest + Testing Library๋ก ์๋ฒ/ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ๋ฅผ ํ ์คํธํ ์ ์๋ค
- MSW๋ก API๋ฅผ ๋ชจํนํด ์ธ๋ถ ์์กด์ฑ ์์ด ํ ์คํธ๋ฅผ ์คํํ ์ ์๋ค
- Playwright๋ก ์ค์ ๋ธ๋ผ์ฐ์ E2E ํ ์คํธ ์๋๋ฆฌ์ค๋ฅผ ์์ฑํ ์ ์๋ค
๐ค ์ ์์์ผ ํ๋๊ฐ
ํ ์คํธ ์์ด ๊ฐ๋ฐํ๋ฉด ์ด๋ฐ ์ผ์ด ์๊ฒจ:
- "๊ฒ์๊ธ ์์ฑ ๊ณ ์ณค๋๋ฐ ๋๊ธ ๊ธฐ๋ฅ์ด ์ ๊นจ์ง์ง?" โ ํ๊ท ๋ฒ๊ทธ
- "๋ฆฌํฉํ ๋งํ๋ค๊ฐ API ์๋ต ํ์์ด ๋ฐ๋ ์ค ๋ชจ๋ฅด๊ณ ๋ฐฐํฌ" โ ํ๋ก๋์ ์ฅ์
- "์ด ํจ์ ๊ฑด๋๋ฆฌ๋ฉด ์ด๋๊ฐ ๊นจ์ง๋์ง ๋ชฐ๋ผ์ ์ฝ๋๋ฅผ ๋ชป ๋ฐ๊พธ๊ฒ ์ด" โ ์ฝ๋ ๊ฒฝ์ง
ํ ์คํธ๋ "๋ด๊ฐ ๋ง๋ ๊ฒ ์ ๋๋ก ๋์ํ๋ค๋ ์ฆ๊ฑฐ"์ด์ "์ด ์ฝ๋๋ฅผ ๋ฐ๊ฟ๋ ๊ด์ฐฎ๋ค๋ ์์ ๊ฐ"์ด์ผ.
๐๏ธ ๋น์ ๋ก ๋จผ์ ์ดํดํ๊ธฐ
๐ง 5์ด์๊ฒ ์ค๋ช ํ๋ค๋ฉด?
์๋์ฐจ ๊ณต์ฅ ๊ฒ์ฌ ๋ผ์ธ์ ์๊ฐํด๋ด. ์ฐจ๋ฅผ ์์ฑํ๊ณ ๋์:
- ๊ฐ ๋ถํ ๊ฒ์ฌ (๋จ์ ํ ์คํธ) โ ๋ธ๋ ์ดํฌ ํจ๋๊ฐ ๊ท๊ฒฉ์ ๋ง๋๊ฐ
- ์กฐ๋ฆฝ ๊ฒ์ฌ (ํตํฉ ํ ์คํธ) โ ๋ธ๋ ์ดํฌ ์ฐ๊ฒฐ์ด ์ ๋๋ก ๋๋๊ฐ
- ์์ด์ ๊ฒ์ฌ (E2E ํ ์คํธ) โ ์ค์ ๋ก ๋ฌ๋ ค๋ณด๊ณ , ๋ธ๋ ์ดํฌ ๋ฐ์ผ๋ฉด ์๋๊ฐ
ํ ์คํธ๋ ๋๊ฐ์. ๋ถํ๋ถํฐ ์ ์ฒด ๊ธฐ๋ฅ๊น์ง ๋จ๊ณ๋ณ๋ก ๊ฒ์ฆํ๋ ๊ฑฐ์ผ.
๐งฉ ํ ์คํธ ํผ๋ผ๋ฏธ๋ โ ์ด๋ค ํ ์คํธ๋ฅผ ์ผ๋ง๋ ๐ข
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
- ๋จ์/ํตํฉ/E2E ํ ์คํธ์ ์ฐจ์ด์ ์ ์ ํ ๋น์จ์ ์ ์ ์๋ค
/\
/E2E\ ์ ์: ๋๋ฆฌ๊ณ ๋น์ธ์ง๋ง ์ค์ ์ฌ์ฉ์ ์๋๋ฆฌ์ค
/------\
/ํตํฉ ํ
์คํธ\ ๋ณดํต: ์ปดํฌ๋ํธ ๊ฐ ์ํธ์์ฉ
/------------\
/ ๋จ์ ํ
์คํธ \ ๋ง์: ๋น ๋ฅด๊ณ ๋
๋ฆฝ์ ์ธ ํจ์/์ปดํฌ๋ํธ ํ
์คํธ
/________________\
| ํ ์คํธ ์ข ๋ฅ | ๋น ๋ฅด๊ธฐ | ์ ๋ขฐ์ฑ | ๋ฌด์์ ํ ์คํธ | ๋๊ตฌ |
|---|---|---|---|---|
| ๋จ์(Unit) | ๋งค์ฐ ๋น ๋ฆ | ๋ถ๋ถ์ | ๊ฐ๋ณ ํจ์, ์ปดํฌ๋ํธ | Jest + Testing Library |
| ํตํฉ(Integration) | ๋ณดํต | ๋์ | ์ปดํฌ๋ํธ + API ์ํธ์์ฉ | Jest + MSW |
| E2E | ๋๋ฆผ | ๋งค์ฐ ๋์ | ์ค์ ์ฌ์ฉ์ ์๋๋ฆฌ์ค | Playwright |
โ๏ธ ํ๊ฒฝ ์ค์ โ Jest + Testing Library ๐ข
# ์ค์น
npm install -D jest jest-environment-jsdom @testing-library/react @testing-library/jest-dom @testing-library/user-event
npm install -D ts-jest @types/jest// jest.config.ts
import type { Config } from 'jest'
import nextJest from 'next/jest.js'
const createJestConfig = nextJest({
dir: './', // Next.js ๋ฃจํธ ๋๋ ํ ๋ฆฌ (next.config.ts, .env ์ฝ๊ธฐ์ฉ)
})
const config: Config = {
coverageProvider: 'v8',
testEnvironment: 'jsdom', // ๋ธ๋ผ์ฐ์ ํ๊ฒฝ ์๋ฎฌ๋ ์ด์
setupFilesAfterFramework: ['<rootDir>/jest.setup.ts'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/$1', // @ ๊ฒฝ๋ก ๋ณ์นญ ๋งคํ
},
}
export default createJestConfig(config)// jest.setup.ts
import '@testing-library/jest-dom' // toBeInTheDocument() ๋ฑ DOM ๋งค์ฒ ์ถ๊ฐ๐ฌ ๋จ์ยทํตํฉ ํ ์คํธ โ ์ปดํฌ๋ํธ์ Server Action ๐ก
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
- ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ์ ๋ ๋๋ง๊ณผ ์ฌ์ฉ์ ์ด๋ฒคํธ๋ฅผ ํ ์คํธํ ์ ์๋ค
- Server Action์ ์ง์ importํด์ ๋ก์ง์ ํ ์คํธํ ์ ์๋ค
๐ฅ๏ธ ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ ํ ์คํธ
// __tests__/components/LoginForm.test.tsx
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { LoginForm } from '@/components/features/auth/LoginForm'
describe('LoginForm', () => {
it('์ด๋ฉ์ผ๊ณผ ๋น๋ฐ๋ฒํธ ์
๋ ฅ ํ๋๊ฐ ๋ ๋๋ผ์ผ ํ๋ค', () => {
render(<LoginForm />)
expect(screen.getByLabelText('์ด๋ฉ์ผ')).toBeInTheDocument()
expect(screen.getByLabelText('๋น๋ฐ๋ฒํธ')).toBeInTheDocument()
expect(screen.getByRole('button', { name: '๋ก๊ทธ์ธ' })).toBeInTheDocument()
})
it('๋น ํผ์ ์ ์ถํ๋ฉด ์ ํจ์ฑ ๊ฒ์ฌ ์๋ฌ๊ฐ ํ์๋ผ์ผ ํ๋ค', async () => {
const user = userEvent.setup()
render(<LoginForm />)
// ์๋ฌด๊ฒ๋ ์
๋ ฅ ์ ํ๊ณ ์ ์ถ
await user.click(screen.getByRole('button', { name: '๋ก๊ทธ์ธ' }))
expect(screen.getByText('์ด๋ฉ์ผ์ ์
๋ ฅํด์ฃผ์ธ์')).toBeInTheDocument()
})
it('์ ํจํ ์ด๋ฉ์ผ๊ณผ ๋น๋ฐ๋ฒํธ๋ฅผ ์
๋ ฅํ๋ฉด ์ ์ถ ๋ฒํผ์ด ํ์ฑํ๋ผ์ผ ํ๋ค', async () => {
const user = userEvent.setup()
render(<LoginForm />)
await user.type(screen.getByLabelText('์ด๋ฉ์ผ'), 'test@example.com')
await user.type(screen.getByLabelText('๋น๋ฐ๋ฒํธ'), 'password123')
expect(screen.getByRole('button', { name: '๋ก๊ทธ์ธ' })).not.toBeDisabled()
})
})โ๏ธ Server Action ํ ์คํธ (๋น์ฆ๋์ค ๋ก์ง)
Server Action์ ๋น์ฆ๋์ค ๋ก์ง์ ์ง์ ํจ์๋ฅผ importํด์ ํ ์คํธํ ์ ์์ด.
// __tests__/lib/actions/post.actions.test.ts
import { createPost } from '@/lib/actions/post.actions'
import { db } from '@/lib/db'
import { getSession } from '@/lib/session'
// DB์ ์ธ์
์ ๋ชจํน (์ค์ DB ์์ด ํ
์คํธ)
jest.mock('@/lib/db', () => ({
db: {
post: {
create: jest.fn(),
},
},
}))
jest.mock('@/lib/session', () => ({
getSession: jest.fn(),
}))
const mockDb = db as jest.Mocked<typeof db>
const mockGetSession = getSession as jest.Mock
describe('createPost Server Action', () => {
beforeEach(() => {
jest.clearAllMocks()
// ๊ธฐ๋ณธ: ๋ก๊ทธ์ธ ์ํ๋ก ์ค์
mockGetSession.mockResolvedValue({ userId: 'user-1', role: 'user' })
})
it('์ ํจํ ๋ฐ์ดํฐ๋ก ๊ฒ์๊ธ์ ์์ฑํด์ผ ํ๋ค', async () => {
const expectedPost = { id: 'post-1', title: 'ํ
์คํธ ๊ฒ์๊ธ', content: '๋ด์ฉ' }
mockDb.post.create.mockResolvedValue(expectedPost as any)
const formData = new FormData()
formData.set('title', 'ํ
์คํธ ๊ฒ์๊ธ')
formData.set('content', '๋ด์ฉ')
const result = await createPost(null, formData)
expect(mockDb.post.create).toHaveBeenCalledWith({
data: { title: 'ํ
์คํธ ๊ฒ์๊ธ', content: '๋ด์ฉ', authorId: 'user-1' },
})
expect(result).toEqual({ success: true })
})
it('๋ก๊ทธ์ธ ์ ํ ์ํ์์๋ ์๋ฌ๋ฅผ ๋ฐํํด์ผ ํ๋ค', async () => {
mockGetSession.mockResolvedValue(null) // ์ธ์
์์
const formData = new FormData()
formData.set('title', '์ ๋ชฉ')
formData.set('content', '๋ด์ฉ')
const result = await createPost(null, formData)
expect(result).toEqual({ error: '๋ก๊ทธ์ธ์ด ํ์ํด์' })
expect(mockDb.post.create).not.toHaveBeenCalled()
})
})๐ MSW๋ก API ๋ชจํน ๐ก
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
- MSW(Mock Service Worker)๋ก ์ค์ API ์์ด fetch ์์ฒญ์ ๋ชจํนํ ์ ์๋ค
- ํ ์คํธ ํ๊ฒฝ์์ API ์๋ต ์๋๋ฆฌ์ค๋ฅผ ์์ ๋กญ๊ฒ ์ ์ดํ ์ ์๋ค
๐ ์ฉ์ด: MSW (Mock Service Worker) โ ์๋น์ค ์์ปค ๋๋ Node.js ์ธํฐ์ ํฐ๋ฅผ ์ฌ์ฉํด HTTP ์์ฒญ์ ๊ฐ๋ก์ฑ๊ณ ๋ชจ์ ์๋ต์ ๋ฐํํ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ผ. ์ค์ ์๋ฒ ์์ด API๋ฅผ ์๋ฎฌ๋ ์ด์ ํ ์ ์์ด.
npm install -D msw// __tests__/mocks/handlers.ts โ API ๋ชจํน ํธ๋ค๋ฌ
import { http, HttpResponse } from 'msw'
export const handlers = [
// GET /api/posts โ ๊ฒ์๊ธ ๋ชฉ๋ก ๋ชจํน
http.get('/api/posts', () => {
return HttpResponse.json([
{ id: '1', title: 'ํด๋ก์ ์คํฐ๋ ๋ชจ์ง', author: '์์' },
{ id: '2', title: 'Next.js ์ฌํ ์คํฐ๋', author: '์ํธ' },
])
}),
// POST /api/posts โ ๊ฒ์๊ธ ์์ฑ ๋ชจํน
http.post('/api/posts', async ({ request }) => {
const body = await request.json() as { title: string; content: string }
return HttpResponse.json(
{ id: 'new-1', ...body },
{ status: 201 }
)
}),
// ์๋ฌ ์๋๋ฆฌ์ค โ ์๋ฒ ์ค๋ฅ ๋ชจํน
http.get('/api/posts/error-test', () => {
return HttpResponse.json(
{ error: '์๋ฒ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ด์' },
{ status: 500 }
)
}),
]// __tests__/mocks/server.ts โ Node.js ํ๊ฒฝ์ฉ MSW ์๋ฒ
import { setupServer } from 'msw/node'
import { handlers } from './handlers'
export const server = setupServer(...handlers)// jest.setup.ts์ ์ถ๊ฐ
import { server } from './__tests__/mocks/server'
beforeAll(() => server.listen()) // ๋ชจ๋ ํ
์คํธ ์ MSW ์๋ฒ ์์
afterEach(() => server.resetHandlers()) // ๊ฐ ํ
์คํธ ํ ํธ๋ค๋ฌ ์ด๊ธฐํ
afterAll(() => server.close()) // ๋ชจ๋ ํ
์คํธ ํ MSW ์๋ฒ ์ข
๋ฃ// __tests__/components/PostList.test.tsx
import { render, screen, waitFor } from '@testing-library/react'
import { server } from '../mocks/server'
import { http, HttpResponse } from 'msw'
import { PostList } from '@/components/features/posts/PostList'
describe('PostList', () => {
it('๊ฒ์๊ธ ๋ชฉ๋ก์ ์ ์ ๋ ๋ํด์ผ ํ๋ค', async () => {
render(<PostList />)
// ๋ก๋ฉ ์ํ ํ์ธ
expect(screen.getByText('๋ก๋ฉ ์ค...')).toBeInTheDocument()
// API ์๋ต ํ ๋ชฉ๋ก ํ์ ๋๊ธฐ
await waitFor(() => {
expect(screen.getByText('ํด๋ก์ ์คํฐ๋ ๋ชจ์ง')).toBeInTheDocument()
expect(screen.getByText('Next.js ์ฌํ ์คํฐ๋')).toBeInTheDocument()
})
})
it('API ์ค๋ฅ ์ ์๋ฌ ๋ฉ์์ง๋ฅผ ํ์ํด์ผ ํ๋ค', async () => {
// ์ด ํ
์คํธ์์๋ง ์ค๋ฅ ์๋ต์ผ๋ก ๋ฎ์ด์
server.use(
http.get('/api/posts', () =>
HttpResponse.json({ error: '์๋ฒ ์ค๋ฅ' }, { status: 500 })
)
)
render(<PostList />)
await waitFor(() => {
expect(screen.getByText('๊ฒ์๊ธ์ ๋ถ๋ฌ์ค๋ ๋ฐ ์คํจํ์ด์')).toBeInTheDocument()
})
})
})๐ญ Playwright E2E ํ ์คํธ ๐ด
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
- Playwright๋ก ์ค์ ๋ธ๋ผ์ฐ์ ์์ ๋ก๊ทธ์ธ ํ๋ก์ฐ๋ฅผ ์๋ํ ํ ์คํธํ ์ ์๋ค
- Page Object Model ํจํด์ผ๋ก ํ ์คํธ ์ฝ๋๋ฅผ ๊ตฌ์กฐํํ ์ ์๋ค
npm install -D @playwright/test
npx playwright install # ๋ธ๋ผ์ฐ์ ๋ฐ์ด๋๋ฆฌ ์ค์น// playwright.config.ts
import { defineConfig, devices } from '@playwright/test'
export default defineConfig({
testDir: './e2e',
baseURL: 'http://localhost:3000',
use: { trace: 'on-first-retry' }, // ์คํจ ์ ํธ๋ ์ด์ค ์ ์ฅ
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'Mobile Chrome', use: { ...devices['Pixel 5'] } },
],
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
})// e2e/auth.spec.ts โ ๋ก๊ทธ์ธ E2E ํ
์คํธ
import { test, expect } from '@playwright/test'
test.describe('์ธ์ฆ ํ๋ก์ฐ', () => {
test('๋ก๊ทธ์ธ ์ฑ๊ณต ์ ๋์๋ณด๋๋ก ์ด๋ํด์ผ ํ๋ค', async ({ page }) => {
await page.goto('/login')
// ์ด๋ฉ์ผ๊ณผ ๋น๋ฐ๋ฒํธ ์
๋ ฅ
await page.getByLabel('์ด๋ฉ์ผ').fill('test@youngsu.community')
await page.getByLabel('๋น๋ฐ๋ฒํธ').fill('testPassword123!')
// ๋ก๊ทธ์ธ ๋ฒํผ ํด๋ฆญ
await page.getByRole('button', { name: '๋ก๊ทธ์ธ' }).click()
// ๋์๋ณด๋๋ก ์ด๋ํ๋์ง ํ์ธ
await expect(page).toHaveURL('/dashboard')
await expect(page.getByText('์๋
ํ์ธ์')).toBeVisible()
})
test('์๋ชป๋ ๋น๋ฐ๋ฒํธ๋ก ๋ก๊ทธ์ธ ์ ์๋ฌ ๋ฉ์์ง๊ฐ ํ์๋ผ์ผ ํ๋ค', async ({ page }) => {
await page.goto('/login')
await page.getByLabel('์ด๋ฉ์ผ').fill('test@youngsu.community')
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/)
})
})// e2e/posts.spec.ts โ ๊ฒ์๊ธ ์์ฑ E2E ํ
์คํธ
import { test, expect } from '@playwright/test'
// ๋ชจ๋ ํ
์คํธ ์ ๋ก๊ทธ์ธ ์ํ ์ค์
test.beforeEach(async ({ page }) => {
await page.goto('/login')
await page.getByLabel('์ด๋ฉ์ผ').fill('test@youngsu.community')
await page.getByLabel('๋น๋ฐ๋ฒํธ').fill('testPassword123!')
await page.getByRole('button', { name: '๋ก๊ทธ์ธ' }).click()
await page.waitForURL('/dashboard')
})
test('๊ฒ์๊ธ์ ์์ฑํ๊ณ ๋ชฉ๋ก์์ ํ์ธํด์ผ ํ๋ค', async ({ page }) => {
await page.goto('/posts/write')
await page.getByLabel('์ ๋ชฉ').fill('Playwright๋ก ํ
์คํธ ์์ฑํ๊ธฐ')
await page.getByLabel('๋ด์ฉ').fill('ํ
์คํธ ์๋ํ๋ ๊ฐ๋ฐ์์ ๋ฐฉํจ๋ง์ด์ผ.')
await page.getByRole('button', { name: '๊ฒ์๊ธ ๋ฑ๋ก' }).click()
// ๊ฒ์๊ธ ๋ชฉ๋ก์ผ๋ก ์ด๋ํ๋์ง ํ์ธ
await expect(page).toHaveURL('/posts')
// ๋ฐฉ๊ธ ์์ฑํ ๊ฒ์๊ธ์ด ๋ชฉ๋ก์ ์๋์ง ํ์ธ
await expect(page.getByText('Playwright๋ก ํ
์คํธ ์์ฑํ๊ธฐ')).toBeVisible()
})๐ฅ ์๋ฌ ํด๊ฒฐ ์นดํ๋ก๊ทธ
โ Cannot find module '@/components/...' โ Jest ๊ฒฝ๋ก ๋ณ์นญ ์ค๋ฅ
์์ธ: tsconfig.json์ @/ ๋ณ์นญ์ด Jest์์ ์ธ์ ์ ๋จ.
ํด๊ฒฐ์ฑ :
// jest.config.ts
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/$1',
}โ window is not defined โ jsdom ํ๊ฒฝ ๋ฌธ์
์์ธ: localStorage, window.matchMedia ๋ฑ ๋ธ๋ผ์ฐ์ ์ ์ฉ API๊ฐ jsdom์ ์์.
ํด๊ฒฐ์ฑ :
// jest.setup.ts์ ์ถ๊ฐ
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(),
removeListener: jest.fn(),
})),
})๐ ์ด๋ฒ์ ๋ฐฐ์ด ๋ด์ฉ ์ด์ ๋ฆฌ
๐ ํ ์คํธ ๋๊ตฌ ์ ํ ๊ธฐ์ค
| ์ํฉ | ๋๊ตฌ |
|---|---|
| ํจ์ ๋ก์ง ๊ฒ์ฆ | Jest |
| ์ปดํฌ๋ํธ UI ๋ ๋๋ง | Testing Library |
| API ๋ชจํน | MSW |
| ์ค์ ์ฌ์ฉ์ ์๋๋ฆฌ์ค | Playwright |
โ ๏ธ ์ ๋ ํ์ง ๋ง ๊ฒ
| ์ํฉ | โ ๋์ ์ | โ ์ข์ ์ |
|---|---|---|
| ๊ตฌํ ์ธ๋ถ์ฌํญ ํ ์คํธ | state.count๋ฅผ ์ง์ ํ์ธ | ์ฌ์ฉ์์๊ฒ ๋ณด์ด๋ ํ ์คํธ ํ์ธ |
| ์ธ๋ถ API ์ค์ ํธ์ถ | ํ ์คํธ์์ ์ค์ DB/API ์ฌ์ฉ | MSW๋ก ๋ชจํน |
| E2E๋ง ์์กด | ๋จ์ ํ ์คํธ ์์ด E2E๋ง | ํผ๋ผ๋ฏธ๋ ๋น์จ ์ ์ง |
๐ ๋ง๋ฌด๋ฆฌ ํด์ฆ
Q1. MSW๋ฅผ ํ ์คํธ์ ๋์ ํ๋ ์ฃผ๋ ์ด์ ๋ ๋ฌด์์ธ๊ฐ?
โ ์ ๋ต: ์ค์ ๋ฐฑ์๋ ์๋ฒ ์์ด๋ HTTP ์์ฒญ/์๋ต ๊ฒฝ๊ณ๋ฅผ ํ์ค์ ์ผ๋ก ๋ชจํนํ๊ธฐ ์ํด์๋ค.
๐ก ์์ธ ํด์ค: fetch๋ฅผ ํจ์ ๋จ์๋ก๋ง ๋ชจํนํ๋ฉด ์ฑ์ด ์ค์ ๋ก ์ด๋ค URL, ๋ฉ์๋, ํค๋๋ฅผ ๋ณด๋ด๋์ง ๋์น๊ธฐ ์ฝ๋ค. MSW๋ ๋คํธ์ํฌ ๊ฒฝ๊ณ์์ ์๋ต์ ๊ฐ๋ก์ฑ๋ฏ๋ก ์ปดํฌ๋ํธ์ ๋ฐ์ดํฐ ๋ก๋ฉ ํ๋ฆ์ ๋ ์ค์ ์ ๊ฐ๊น๊ฒ ๊ฒ์ฆํ ์ ์๋ค.
Q2. Playwright E2E๋ก ๊ฒ์ฆํ๊ธฐ ๊ฐ์ฅ ์ข์ ๋์์ ๋ฌด์์ธ๊ฐ?
โ ์ ๋ต: ๋ก๊ทธ์ธ, ๊ธ ์์ฑ, ๋ชฉ๋ก ๋ฐ์์ฒ๋ผ ์ฌ๋ฌ ๋ ์ด์ด๋ฅผ ํต๊ณผํ๋ ํต์ฌ ์ฌ์ฉ์ ํ๋ก์ฐ๋ค.
๐ก ์์ธ ํด์ค: ์ ํธ ํจ์ ๋ฐํ๊ฐ์ด๋ Zod ์คํค๋ง๋ ๋จ์ ํ ์คํธ๊ฐ ๋น ๋ฅด๊ณ ์ ํํ๋ค. E2E๋ ๋น์ฉ์ด ํฌ๋ฏ๋ก ์ฌ์ฉ์๊ฐ ์ค์ ๋ก ๊ฒช๋ ์ค์ํ ๊ฒฝ๋ก, ํนํ ๋ผ์ฐํ /์ธ์ฆ/์๋ฒ ์ก์ /์บ์ ๋ฐ์์ด ํจ๊ป ์ฝํ ํ๋ฆ์ ์จ์ผ ํ๋ค.
Q3. ์์ฒ ์ด์ ํ ์คํธ ํ์: Server Action์ด ์ฑ๊ณตํ๋ฉด revalidatePath('/posts')๋ฅผ ํธ์ถํ๊ณ ๋ชฉ๋ก์ผ๋ก ์ด๋ํ๋ค. ์ด๋ค ํ ์คํธ ์กฐํฉ์ด ํ์ค์ ์ผ๊น?
โ ์ ๋ต: ์ก์ ์ ์ ๋ ฅ ๊ฒ์ฆ๊ณผ ๊ถํ์ ๋จ์/ํตํฉ ํ ์คํธ๋ก, ์ฌ์ฉ์๊ฐ ํผ ์ ์ถ ํ ๋ชฉ๋ก์์ ์ ๊ธ์ ๋ณด๋ ํ๋ฆ์ Playwright๋ก ๊ฒ์ฆํ๋ค.
๐ก ์์ธ ํด์ค: ํ ์ข ๋ฅ์ ํ ์คํธ๋ก ๋ชจ๋ ๊ฒ์ ๋ฎ์ผ๋ ค ํ๋ฉด ๋๋ฆฌ๊ฑฐ๋ ์ทจ์ฝํด์ง๋ค. ์ํธ๋ผ๋ฉด ํผ๋ผ๋ฏธ๋๋ณด๋ค "์คํจํ์ ๋ ์์ธ์ ๋นจ๋ฆฌ ์ฐพ๋๊ฐ"๋ฅผ ๋ฌป๋๋ค. ์์ ๋ก์ง์ ์๊ฒ, ์ฌ์ฉ์ ์ ๋ขฐ๋ฅผ ์ข์ฐํ๋ ์ฐ๊ฒฐ๋ถ๋ E2E๋ก ํ์ธํ๋ ๊ท ํ์ด ํ์ํ๋ค.
๐ฃ ์์ฒ ์ด์ ํด๊ทผ ์ผ๊ธฐ
์ค๋์ ํ ์คํธ๋ฅผ ๋ง์ด ์ฐ๋ ๊ฒ๋ณด๋ค ์ด๋์ ์ด๋ค ํ ์คํธ๋ฅผ ๋์ง ์ ํ๋ ๊ฒ ๋ ์ด๋ ต๋ค๋ ๊ฑธ ๋๊ผ๋ค. ๋จ์ ํ ์คํธ๋ ๋น ๋ฅธ ํผ๋๋ฐฑ์ ์ฃผ๊ณ , MSW๋ API ๊ฒฝ๊ณ๋ฅผ ์์ ์ ์ผ๋ก ๋ง๋ค๊ณ , Playwright๋ ์ฌ์ฉ์์ ์ฝ์์ ๋๊น์ง ํ์ธํด ์ค๋ค.
๐ก "์ข์ ํ ์คํธ ์ ๋ต์ ์ปค๋ฒ๋ฆฌ์ง ์ซ์๋ณด๋ค ์คํจ ์์น๋ฅผ ๋นจ๋ฆฌ ์ขํ ์ฃผ๋ ๊ตฌ์กฐ๋ค."
๋ค์๋ถํฐ ๊ธฐ๋ฅ์ ๋ง๋ค ๋ ํ ์คํธ๋ฅผ ๋ง์ง๋ง์ ๋ถ์ด์ง ์๊ฒ ๋ค. ๋จผ์ ์ด ๋ก์ง์ด ์์ ํจ์์ธ์ง, ์๋ฒ ๊ฒฝ๊ณ๋ฅผ ํ๋์ง, ์ฌ์ฉ์๊ฐ ๋ฐ๋์ ์ฑ๊ณตํด์ผ ํ๋ ํ๋ฆ์ธ์ง ๋๋๊ณ ๊ทธ์ ๋ง๋ ๋๊ตฌ๋ฅผ ์ ํํ๊ฒ ๋ค.