Files
web-inspector/.claude/skills/testing-standards.md
jungwoo choi c37cda5b13 Initial commit: 프로젝트 초기 구성
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 16:10:57 +09:00

8.1 KiB

테스트 작성 표준 (Testing Standards)

이 프로젝트의 테스트 작성 패턴입니다.

Python (pytest)

설치

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 설정

[pytest]
asyncio_mode = auto
testpaths = tests
python_files = test_*.py
python_functions = test_*

Fixture 패턴 (conftest.py)

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"]}]
        }
    }

단위 테스트 예시

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

비동기 테스트 예시

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 사용 예시

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()
        # ... 테스트 수행

테스트 실행

# 전체 테스트
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)

설치

npm install --save-dev jest @types/jest ts-jest

jest.config.js

module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  roots: ['<rootDir>/src'],
  testMatch: ['**/*.test.ts', '**/*.spec.ts'],
  collectCoverageFrom: [
    'src/**/*.{ts,tsx}',
    '!src/**/*.d.ts',
  ],
}

단위 테스트 예시

// 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 컴포넌트 테스트

// 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)

// 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')
  })
})

테스트 실행

# 전체 테스트
npm test

# 감시 모드
npm test -- --watch

# 커버리지
npm test -- --coverage

# 특정 파일만
npm test -- Button.test.tsx

E2E 테스트 (Playwright)

설치

npm install --save-dev @playwright/test
npx playwright install

playwright.config.ts

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 테스트 예시

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

# .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