# 테스트 작성 표준 (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: ['/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() expect(screen.getByText('Click me')).toBeInTheDocument() }) it('calls onClick handler', () => { const handleClick = jest.fn() render() fireEvent.click(screen.getByText('Click me')) expect(handleClick).toHaveBeenCalledTimes(1) }) it('applies variant styles', () => { render() 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 ```