Ch8. 테스트 전략
모노레포 환경 테스트 피라미드, Vitest 유닛, Playwright e2e
모노레포 테스트 피라미드
| 레벨 | 도구 | 범위 | CI 실행 조건 |
|---|---|---|---|
| 유닛 | Vitest | 패키지/앱 내 함수·컴포넌트 | 모든 PR |
| 통합 | Vitest + MSW | 앱 내 API 연동·데이터 흐름 | 해당 앱 변경 시 |
| E2E | Playwright | 전체 사용자 시나리오 | main 머지 전 |
Vitest Workspace 설정
Vitest의 workspace 기능으로 모노레포 전체를 하나의 테스트 명령으로 실행합니다:
// vitest.workspace.ts (루트)
import { defineWorkspace } from 'vitest/config'
export default defineWorkspace(['apps/*/vitest.config.ts', 'packages/*/vitest.config.ts'])// packages/utils/vitest.config.ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
name: '@acme/utils',
include: ['src/**/*.test.ts'],
},
})// apps/web/vitest.config.ts
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
test: {
name: 'web',
environment: 'jsdom',
include: ['**/*.test.{ts,tsx}'],
setupFiles: ['./tests/setup.ts'],
},
})# 전체 테스트
vitest run
# 특정 패키지만
vitest run --project=@acme/utils
# 워치 모드 (개발 중)
vitest --project=webPlaywright E2E 설정
// e2e/playwright.config.ts
import { defineConfig, devices } from '@playwright/test'
export default defineConfig({
testDir: './tests',
fullyParallel: true,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 4 : undefined,
reporter: process.env.CI ? 'github' : 'html',
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'mobile', use: { ...devices['Pixel 7'] } },
],
webServer: {
command: 'turbo run dev --filter=web',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
})// e2e/tests/auth-flow.spec.ts
import { test, expect } from '@playwright/test'
test('로그인 → 대시보드 접근', async ({ page }) => {
await page.goto('/login')
await page.fill('[name="email"]', 'test@acme.com')
await page.fill('[name="password"]', 'test1234')
await page.click('button[type="submit"]')
await expect(page).toHaveURL('/dashboard')
await expect(page.locator('h1')).toContainText('대시보드')
})컴포넌트 테스트 패턴
// packages/ui/src/button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react'
import { Button } from './button'
import { expect, test, vi } from 'vitest'
test('클릭 이벤트 전달', () => {
const onClick = vi.fn()
render(<Button onClick={onClick}>확인</Button>)
fireEvent.click(screen.getByText('확인'))
expect(onClick).toHaveBeenCalledOnce()
})
test('variant별 스타일 적용', () => {
const { rerender } = render(<Button variant="primary">버튼</Button>)
expect(screen.getByRole('button')).toHaveClass(/primary/)
rerender(<Button variant="ghost">버튼</Button>)
expect(screen.getByRole('button')).toHaveClass(/ghost/)
})CI에서 테스트 캐싱
// turbo.json
{
"tasks": {
"test": {
"dependsOn": ["^build"],
"inputs": ["src/**", "tests/**", "vitest.config.*", "playwright.config.*"],
"outputs": ["coverage/**", "test-results/**", "playwright-report/**"],
},
},
}inputs를 정확히 지정하면, 테스트 대상 코드가 변경되지 않았을 때 캐시된 결과를 재사용합니다. 테스트 실행 자체를 건너뛰므로 CI 시간이 대폭 단축됩니다.
2025~2026 테스트 도구 변경
Vitest 4.x
| 항목 | 변경 |
|---|---|
| Browser Mode 정식 라인 | Playwright/WebDriverIO 기반 실제 브라우저 테스트 |
| 인라인 Workspace | vitest.workspace.ts 없이 vitest.config.ts에서 워크스페이스 정의 가능 |
| 성능 개선 | 대규모 모노레포에서 워치 모드 속도 향상 |
Playwright 1.50+
| 항목 | 변경 |
|---|---|
| Aria snapshots | 접근성 트리 기반 스냅샷 테스트 |
| 향상된 병렬 실행 | 테스트 샤딩 개선으로 CI 시간 단축 |
| 컴포넌트 테스트 | React/Vue/Svelte 컴포넌트 직접 테스트 안정화 |
참고 문서
- 브라우저 러너를 넘어 gate, flaky, KPI, incident까지 포함한 테스트 환경 운영 심화는 에이전틱 테스트 환경 엔지니어링을 참고하세요.
- Vitest: Workspace (영어)
- Playwright: Getting Started (영어)
- Testing Library: React (영어)
- MSW: Getting Started (영어)