feat: Drama Studio 프로젝트 초기 구조 설정
- FastAPI 백엔드 (audio-studio-api) - Next.js 프론트엔드 (audio-studio-ui) - Qwen3-TTS 엔진 (audio-studio-tts) - MusicGen 서비스 (audio-studio-musicgen) - Docker Compose 개발/운영 환경 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
360
.claude/skills/testing-standards.md
Normal file
360
.claude/skills/testing-standards.md
Normal file
@ -0,0 +1,360 @@
|
||||
# 테스트 작성 표준 (Testing Standards)
|
||||
|
||||
이 프로젝트의 테스트 작성 패턴입니다.
|
||||
|
||||
## Python (pytest)
|
||||
|
||||
### 설치
|
||||
```bash
|
||||
pip install pytest pytest-asyncio pytest-cov
|
||||
```
|
||||
|
||||
### 디렉토리 구조
|
||||
```
|
||||
service/
|
||||
├── worker.py
|
||||
├── service.py
|
||||
├── tests/
|
||||
│ ├── __init__.py
|
||||
│ ├── conftest.py # 공통 fixture
|
||||
│ ├── test_worker.py
|
||||
│ └── test_service.py
|
||||
└── pytest.ini
|
||||
```
|
||||
|
||||
### pytest.ini 설정
|
||||
```ini
|
||||
[pytest]
|
||||
asyncio_mode = auto
|
||||
testpaths = tests
|
||||
python_files = test_*.py
|
||||
python_functions = test_*
|
||||
```
|
||||
|
||||
### Fixture 패턴 (conftest.py)
|
||||
```python
|
||||
import pytest
|
||||
import asyncio
|
||||
from motor.motor_asyncio import AsyncIOMotorClient
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def event_loop():
|
||||
"""세션 범위의 이벤트 루프"""
|
||||
loop = asyncio.get_event_loop_policy().new_event_loop()
|
||||
yield loop
|
||||
loop.close()
|
||||
|
||||
@pytest.fixture
|
||||
async def mongodb():
|
||||
"""테스트용 MongoDB 연결"""
|
||||
client = AsyncIOMotorClient("mongodb://localhost:27017")
|
||||
db = client["test_db"]
|
||||
yield db
|
||||
# 테스트 후 정리
|
||||
await client.drop_database("test_db")
|
||||
client.close()
|
||||
|
||||
@pytest.fixture
|
||||
def sample_article():
|
||||
"""테스트용 기사 데이터"""
|
||||
return {
|
||||
"title": "Test Article",
|
||||
"summary": "Test summary",
|
||||
"entities": {
|
||||
"people": [{"name": "Elon Musk", "context": ["Tesla", "CEO"]}],
|
||||
"organizations": [{"name": "Tesla", "context": ["EV", "automotive"]}]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 단위 테스트 예시
|
||||
```python
|
||||
import pytest
|
||||
from service import calculate_biocode
|
||||
|
||||
def test_calculate_biocode_basic():
|
||||
"""바이오코드 계산 기본 테스트"""
|
||||
result = calculate_biocode(1990, 5, 15)
|
||||
assert result is not None
|
||||
assert len(result) == 4 # g코드 2자리 + s코드 2자리
|
||||
|
||||
def test_calculate_biocode_edge_cases():
|
||||
"""경계값 테스트"""
|
||||
# 연초
|
||||
result = calculate_biocode(1990, 1, 1)
|
||||
assert result.endswith("60") # 대설
|
||||
|
||||
# 연말
|
||||
result = calculate_biocode(1990, 12, 31)
|
||||
assert result is not None
|
||||
```
|
||||
|
||||
### 비동기 테스트 예시
|
||||
```python
|
||||
import pytest
|
||||
from wikipedia_service import WikipediaService
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_person_info():
|
||||
"""인물 정보 조회 테스트"""
|
||||
service = WikipediaService()
|
||||
|
||||
try:
|
||||
info = await service.get_person_info(
|
||||
"Elon Musk",
|
||||
context=["Tesla", "SpaceX"]
|
||||
)
|
||||
|
||||
assert info is not None
|
||||
assert info.name == "Elon Musk"
|
||||
assert info.wikipedia_url is not None
|
||||
finally:
|
||||
await service.close()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_organization_info_with_logo():
|
||||
"""조직 로고 우선 조회 테스트"""
|
||||
service = WikipediaService()
|
||||
|
||||
try:
|
||||
info = await service.get_organization_info(
|
||||
"Apple Inc.",
|
||||
context=["technology", "iPhone"]
|
||||
)
|
||||
|
||||
assert info is not None
|
||||
assert info.image_urls # 로고 이미지가 있어야 함
|
||||
finally:
|
||||
await service.close()
|
||||
```
|
||||
|
||||
### Mock 사용 예시
|
||||
```python
|
||||
from unittest.mock import AsyncMock, patch
|
||||
import pytest
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_worker_process_job():
|
||||
"""워커 작업 처리 테스트 (외부 API 모킹)"""
|
||||
with patch('worker.WikipediaService') as mock_service:
|
||||
mock_instance = AsyncMock()
|
||||
mock_instance.get_person_info.return_value = PersonInfo(
|
||||
name="Test Person",
|
||||
birth_date="1990-01-15",
|
||||
verified=True
|
||||
)
|
||||
mock_service.return_value = mock_instance
|
||||
|
||||
worker = WikipediaEnrichmentWorker()
|
||||
# ... 테스트 수행
|
||||
```
|
||||
|
||||
### 테스트 실행
|
||||
```bash
|
||||
# 전체 테스트
|
||||
pytest
|
||||
|
||||
# 커버리지 포함
|
||||
pytest --cov=. --cov-report=html
|
||||
|
||||
# 특정 테스트만
|
||||
pytest tests/test_service.py -v
|
||||
|
||||
# 특정 함수만
|
||||
pytest tests/test_service.py::test_calculate_biocode_basic -v
|
||||
```
|
||||
|
||||
## JavaScript/TypeScript (Jest)
|
||||
|
||||
### 설치
|
||||
```bash
|
||||
npm install --save-dev jest @types/jest ts-jest
|
||||
```
|
||||
|
||||
### jest.config.js
|
||||
```javascript
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
roots: ['<rootDir>/src'],
|
||||
testMatch: ['**/*.test.ts', '**/*.spec.ts'],
|
||||
collectCoverageFrom: [
|
||||
'src/**/*.{ts,tsx}',
|
||||
'!src/**/*.d.ts',
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
### 단위 테스트 예시
|
||||
```typescript
|
||||
// utils.test.ts
|
||||
import { cn, formatDate } from './utils'
|
||||
|
||||
describe('cn utility', () => {
|
||||
it('should merge class names', () => {
|
||||
const result = cn('foo', 'bar')
|
||||
expect(result).toBe('foo bar')
|
||||
})
|
||||
|
||||
it('should handle conditional classes', () => {
|
||||
const result = cn('foo', false && 'bar', 'baz')
|
||||
expect(result).toBe('foo baz')
|
||||
})
|
||||
})
|
||||
|
||||
describe('formatDate', () => {
|
||||
it('should format date correctly', () => {
|
||||
const date = new Date('2024-01-15')
|
||||
expect(formatDate(date)).toBe('2024-01-15')
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### React 컴포넌트 테스트
|
||||
```typescript
|
||||
// Button.test.tsx
|
||||
import { render, screen, fireEvent } from '@testing-library/react'
|
||||
import { Button } from './Button'
|
||||
|
||||
describe('Button', () => {
|
||||
it('renders correctly', () => {
|
||||
render(<Button>Click me</Button>)
|
||||
expect(screen.getByText('Click me')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('calls onClick handler', () => {
|
||||
const handleClick = jest.fn()
|
||||
render(<Button onClick={handleClick}>Click me</Button>)
|
||||
fireEvent.click(screen.getByText('Click me'))
|
||||
expect(handleClick).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('applies variant styles', () => {
|
||||
render(<Button variant="destructive">Delete</Button>)
|
||||
const button = screen.getByText('Delete')
|
||||
expect(button).toHaveClass('bg-destructive')
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### API 테스트 (MSW Mock)
|
||||
```typescript
|
||||
// api.test.ts
|
||||
import { rest } from 'msw'
|
||||
import { setupServer } from 'msw/node'
|
||||
import { fetchArticles } from './api'
|
||||
|
||||
const server = setupServer(
|
||||
rest.get('/api/articles', (req, res, ctx) => {
|
||||
return res(
|
||||
ctx.json({
|
||||
items: [{ id: '1', title: 'Test Article' }],
|
||||
total: 1
|
||||
})
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
beforeAll(() => server.listen())
|
||||
afterEach(() => server.resetHandlers())
|
||||
afterAll(() => server.close())
|
||||
|
||||
describe('fetchArticles', () => {
|
||||
it('fetches articles successfully', async () => {
|
||||
const articles = await fetchArticles()
|
||||
expect(articles.items).toHaveLength(1)
|
||||
expect(articles.items[0].title).toBe('Test Article')
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### 테스트 실행
|
||||
```bash
|
||||
# 전체 테스트
|
||||
npm test
|
||||
|
||||
# 감시 모드
|
||||
npm test -- --watch
|
||||
|
||||
# 커버리지
|
||||
npm test -- --coverage
|
||||
|
||||
# 특정 파일만
|
||||
npm test -- Button.test.tsx
|
||||
```
|
||||
|
||||
## E2E 테스트 (Playwright)
|
||||
|
||||
### 설치
|
||||
```bash
|
||||
npm install --save-dev @playwright/test
|
||||
npx playwright install
|
||||
```
|
||||
|
||||
### playwright.config.ts
|
||||
```typescript
|
||||
import { defineConfig } from '@playwright/test'
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './e2e',
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
use: {
|
||||
baseURL: 'http://localhost:3000',
|
||||
trace: 'on-first-retry',
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### E2E 테스트 예시
|
||||
```typescript
|
||||
// e2e/dashboard.spec.ts
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test.describe('Dashboard', () => {
|
||||
test('should display article list', async ({ page }) => {
|
||||
await page.goto('/dashboard')
|
||||
await expect(page.getByRole('heading', { name: 'Articles' })).toBeVisible()
|
||||
await expect(page.getByRole('table')).toBeVisible()
|
||||
})
|
||||
|
||||
test('should filter articles by keyword', async ({ page }) => {
|
||||
await page.goto('/dashboard')
|
||||
await page.fill('[placeholder="Search..."]', 'technology')
|
||||
await page.click('button:has-text("Search")')
|
||||
await expect(page.locator('table tbody tr')).toHaveCount(5)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## CI/CD 통합
|
||||
|
||||
### GitHub Actions
|
||||
```yaml
|
||||
# .github/workflows/test.yml
|
||||
name: Tests
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
python-tests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- run: pip install -r requirements.txt
|
||||
- run: pytest --cov
|
||||
|
||||
js-tests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '20'
|
||||
- run: npm ci
|
||||
- run: npm test -- --coverage
|
||||
```
|
||||
Reference in New Issue
Block a user