๐ 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(Mock Service Worker)๋ฅผ ์ฌ์ฉํ๋ ์ฃผ๋ ์ด์ ๋?
- A) ํ๋ก๋์ API ์๋๋ฅผ ๋์ด๊ธฐ ์ํด
- B) ํ ์คํธ ํ๊ฒฝ์์ ์ค์ ์๋ฒ ์์ด HTTP ์์ฒญ์ ๋ชจํนํ๊ธฐ ์ํด
- C) ์๋น์ค ์์ปค๋ก ์คํ๋ผ์ธ ๊ธฐ๋ฅ์ ๊ตฌํํ๊ธฐ ์ํด
- D) API ์๋ต์ ์บ์ฑํ๊ธฐ ์ํด
โ ์ ๋ต: B
ํด์ค: MSW๋ ํ ์คํธ์์ ์ค์ ์๋ฒ ์์ด ์ํ๋ API ์๋ต์ ์๋ฎฌ๋ ์ด์ ํด์ค. ์ค์ DB๋ ์ธ๋ถ ์๋น์ค์ ์์กดํ์ง ์์๋ ๋ผ์ ํ ์คํธ๊ฐ ๋น ๋ฅด๊ณ ์์ ์ ์ด์ผ.
๐ ํต์ฌ ๊ธฐ์ต๋ฒ: MSW = "์งํ ์๋ฒ". ํ ์คํธ์ฉ ์๋ต์ ๋ง์๋๋ก ์ค์ ํ ์ ์์ด.
Q2. ๋ค์ ์ค E2E ํ ์คํธ๋ก ๊ฒ์ฆํ๊ธฐ ๊ฐ์ฅ ์ ํฉํ ๊ฒ์?
- A) ํน์ ์ ํธ ํจ์์ ๋ฐํ๊ฐ
- B) ๋ฒํผ ์ปดํฌ๋ํธ์ CSS ์คํ์ผ
- C) ๋ก๊ทธ์ธ โ ๊ฒ์๊ธ ์์ฑ โ ๋ชฉ๋ก ํ์ธ๊น์ง์ ์ฌ์ฉ์ ํ๋ก์ฐ
- D) Zod ์คํค๋ง ์ ํจ์ฑ ๊ฒ์ฌ ๋ก์ง
โ ์ ๋ต: C
ํด์ค: E2E๋ ์ค์ ์ฌ์ฉ์ ์๋๋ฆฌ์ค ์ ์ฒด ํ๋ก์ฐ๋ฅผ ๊ฒ์ฆํด. A, D๋ ๋จ์ ํ ์คํธ, B๋ ์ปดํฌ๋ํธ ํ ์คํธ๊ฐ ์ ํฉํด.
๐ ํต์ฌ ๊ธฐ์ต๋ฒ: E2E = "์ฌ๋์ด ์ฐ๋ ๊ฒ์ฒ๋ผ ์ฒ์๋ถํฐ ๋๊น์ง."
Q3. ์น๊ตฌ์๊ฒ ์ค๋ช ํ๋ค๋ฉด?
ํ ์คํธ ํผ๋ผ๋ฏธ๋์์ ๋จ์ ํ ์คํธ๊ฐ ๊ฐ์ฅ ๋ง์์ผ ํ๋ ์ด์ ๋ฅผ ๋น์ ๋ก ์ค๋ช ํด๋ด.
์์ ๋ต๋ณ:
"๊ฑด๋ฌผ ๊ฒ์ฌ ๋น์ ์ผ. ์์ฑ๋ ๊ฑด๋ฌผ ์ ์ฒด๋ฅผ ํ ๋ฒ์ ๊ฒ์ฌ(E2E)ํ๋ ค๋ฉด ์๊ฐ์ด ์ค๋ ๊ฑธ๋ฆฌ๊ณ ๋น์ธ. ๊ทผ๋ฐ ๋ฒฝ๋ ํ๋ํ๋๋ฅผ ๋จผ์ ๊ฒ์ฌ(๋จ์ ํ ์คํธ)ํด๋๋ฉด ๊ฑด๋ฌผ์ ์ง์ผ๋ฉด์ ๋ฐ๋ก ๋ฌธ์ ๋ฅผ ์ก์ ์ ์์ด. ๋ฒฝ๋์ด ํผํผํ๋ฉด ๊ฑด๋ฌผ๋ ํผํผํ ๊ฐ๋ฅ์ฑ์ด ๋์. ๋จ์ ํ ์คํธ๊ฐ ๋ง์ ๊ฑด '๋ถํ ํ์ง ๋ณด์ฆ์ด ๋๊ป๋ค'๋ ๋ป์ด์ผ."
๐ฃ ์์ฒ ์ด์ ํด๊ทผ ์ผ๊ธฐ
์ค๋์ ์ ๋ง ๊ฐ๋ฐ์์ ๊ฐ์ฅ ๋ ๋ ํ ๋ฐฉํจ๋ง์ด๋ผ ๋ถ๋ฆฌ๋ 'Testing Strategy' ๋ฅผ ๋ฐฐ์ฐ๋ฉด์ ๋ง์์ด ํ๊ฒฐ ๊ฐ๋ฒผ์์ง ๋ ์ด์ผ! ๊ทธ๋์์ "์ฝ๋๋ฅผ ๊ณ ์น๋ฉด ์ด๋๊ฐ ๊นจ์ง์ง ๋ชฐ๋ผ" ๋ผ๋ฉฐ ๋ถ์ํดํ๋๋ฐ, ์ด์ ๋ Jest์ Playwright๋ผ๋ ๋ ๋ ํ ์๊ตฐ์ด ์๊ฒผ๊ฑฐ๋ .
๐ก ์ค๋์ ๊ตํ: "ํ ์คํธ๋ ๋จ์ํ ๊ฒ์ฆ์ด ์๋๋ผ, ๋ฏธ๋์ ๋๋ฅผ ์ง์ผ์ฃผ๋ ๋ณดํ์ด๋ค. ๋จ์ ํ ์คํธ๋ก ๋ถํ์ ๋จ๋จํ ํ๊ณ , E2E ํ ์คํธ๋ก ์ฌ์ฉ์ ์๋๋ฆฌ์ค๋ฅผ ๋๊น์ง ์ฌ์ํ์!"
์ํธ ๋ฆฌ๋ ๋์ด ๊ฑด๋ฌผ ๋ฒฝ๋ ๋น์ ๋ฅผ ๋ค์ด ์ค๋ช ํด ์ฃผ์ค ๋, ์ ํ ์คํธ ํผ๋ผ๋ฏธ๋๊ฐ ์ค์ํ์ง ๋จ๋ฒ์ ์ดํด๊ฐ ๊ฐ๋๋ผ. ๋จ์ํ ๋ฒ๊ทธ๋ฅผ ์ก๋ ๊ฑธ ๋์ด, ๋ด ์ฝ๋๋ฅผ ๋ฏฟ๊ณ ๋ฐฐํฌํ ์ ์๋ ์ฉ๊ธฐ๋ฅผ ์ฃผ๋ ๊ณผ์ ์ด๋ผ๋ ๊ฑธ ๊นจ๋ฌ์์ด. ์ค๋ ๋๋ฌด ๋ ๋ ํ๊ฒ ํ ์คํธ ์ฝ๋๋ฅผ ์งฐ๋๋ ๋ฟ๋ฏํจ์ด ๋ฐ๋ ค์ค๋ค. ํด๊ทผ๊ธธ์ ๋ด๊ฐ ์ข์ํ๋ ์์ ๋ฒ๊ฑฐ ํ๋ ์ฌ ๋จน์ผ๋ฉด์ ์ค๋ ์ง '์๋ฒฝํ' ํ ์คํธ ๋ก์ง์ ๊ณฑ์น์ด๋ด์ผ๊ฒ ์ด. ๋ด์ผ์ ๋ '๊ฒฌ๊ณ ํ' ์ํคํ ์ฒ๋ฅผ ์ค๊ณํ๋ ๊ฐ๋ฐ์๊ฐ ๋ ๊ฑฐ์ผ! ๐ฃ