Files
black-ink/.claude/skills/testing-standards.md
jungwoo choi 0ee5d066b4 Initial commit: 프로젝트 초기 구성
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 08:21:50 +09:00

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
```