- 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>
361 lines
8.1 KiB
Markdown
361 lines
8.1 KiB
Markdown
# 테스트 작성 표준 (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
|
|
```
|