| name | tdd-workflow |
| description | 새 기능을 작성하거나 버그를 수정하거나 코드를 리팩터링할 때 이 스킬을 사용하세요. 단위, 통합 및 E2E 테스트를 포함하여 80% 이상의 커버리지를 가진 테스트 주도 개발을 강제합니다.
|
| metadata | {"origin":"ECC","version":"1.0"} |
테스트 주도 개발(TDD) 워크플로우
이 스킬은 모든 코드 개발이 포괄적인 테스트 커버리지를 갖춘 TDD 원칙을 따르도록 보장합니다.
활성화 시기
- 새 기능 또는 함수 작성 시
- 버그 또는 이슈 수정 시
- 기존 코드 리팩터링 시
- API 엔드포인트 추가 시
- 새 컴포넌트 생성 시
핵심 원칙
1. 코드 작성 전 테스트 작성
항상 테스트를 먼저 작성한 다음, 테스트를 통과시키기 위한 코드를 구현하십시오.
2. 커버리지 요구 사항
- 최소 80% 커버리지 (단위 + 통합 + E2E)
- 모든 엣지 케이스 포함
- 에러 시나리오 테스트
- 경계 조건 검증
3. 테스트 유형
단위 테스트 (Unit Tests)
- 개별 함수 및 유틸리티
- 컴포넌트 로직
- 순수 함수
- 헬퍼 및 유틸리티
통합 테스트 (Integration Tests)
- API 엔드포인트
- 데이터베이스 작업
- 서비스 간 상호작용
- 외부 API 호출
E2E 테스트 (Playwright)
- 핵심 사용자 흐름
- 전체 워크플로우
- 브라우저 자동화
- UI 상호작용
TDD 워크플로우 단계
1단계: 사용자 여정(User Journeys) 작성
[역할]로서, 나는 [이점]을 위해 [행동]을 하고 싶다.
예시:
사용자로서, 나는 정확한 키워드가 없어도 관련 마켓을 찾을 수 있도록
의미론적(semantically)으로 마켓을 검색하고 싶다.
2단계: 테스트 케이스 생성
각 사용자 여정에 대해 포괄적인 테스트 케이스를 생성합니다.
describe('의미론적 검색', () => {
it('쿼리에 대해 관련 마켓을 반환한다', async () => {
})
it('빈 쿼리를 우아하게 처리한다', async () => {
})
it('Redis를 사용할 수 없을 때 부분 문자열 검색으로 대체한다', async () => {
})
it('유사도 점수에 따라 결과를 정렬한다', async () => {
})
})
3단계: 테스트 실행 (실패해야 함)
npm test
4단계: 코드 구현
테스트를 통과시키기 위한 최소한의 코드를 작성합니다.
export async function searchMarkets(query: string) {
}
5단계: 테스트 다시 실행
npm test
6단계: 리팩터링
테스트를 통과시킨 상태를 유지하면서 코드 품질을 개선합니다.
- 중복 제거
- 이름 개선
- 성능 최적화
- 가독성 향상
7단계: 커버리지 확인
npm run test:coverage
테스트 패턴
단위 테스트 패턴 (Jest/Vitest)
import { render, screen, fireEvent } from '@testing-library/react'
import { Button } from './Button'
describe('Button 컴포넌트', () => {
it('올바른 텍스트로 렌더링된다', () => {
render(<Button>Click me</Button>)
expect(screen.getByText('Click me')).toBeInTheDocument()
})
it('클릭 시 onClick이 호출된다', () => {
const handleClick = jest.fn()
render(<Button onClick={handleClick}>Click</Button>)
fireEvent.click(screen.getByRole('button'))
expect(handleClick).toHaveBeenCalledTimes(1)
})
it('disabled 프로퍼티가 true일 때 비활성화된다', () => {
render(<Button disabled>Click</Button>)
expect(screen.()).()
})
})
API 통합 테스트 패턴
import { NextRequest } from 'next/server'
import { GET } from './route'
describe('GET /api/markets', () => {
it('마켓을 성공적으로 반환한다', async () => {
const request = new NextRequest('http://localhost/api/markets')
const response = await GET(request)
const data = await response.json()
expect(response.status).toBe(200)
expect(data.success).toBe(true)
expect(Array.isArray(data.data)).toBe(true)
})
it('쿼리 파라미터를 검증한다', async () => {
const request = new NextRequest('http://localhost/api/markets?limit=invalid')
const response = await GET(request)
expect(response.status).()
})
(, () => {
request = ()
})
})
E2E 테스트 패턴 (Playwright)
import { test, expect } from '@playwright/test'
test('사용자가 마켓을 검색하고 필터링할 수 있다', async ({ page }) => {
await page.goto('/')
await page.click('a[href="/markets"]')
await expect(page.locator('h1')).toContainText('Markets')
await page.fill('input[placeholder="Search markets"]', 'election')
await page.waitForTimeout(600)
const results = page.locator('[data-testid="market-card"]')
await expect(results).toHaveCount(5, { timeout: 5000 })
const firstResult = results.first()
await expect(firstResult).toContainText('election', { ignoreCase: true })
page.()
(results).()
})
(, ({ page }) => {
page.()
page.(, )
page.(, )
page.(, )
page.()
(page.()).()
(page).()
})
테스트 파일 구성
src/
├── components/
│ ├── Button/
│ │ ├── Button.tsx
│ │ ├── Button.test.tsx # 단위 테스트
│ │ └── Button.stories.tsx # Storybook
│ └── MarketCard/
│ ├── MarketCard.tsx
│ └── MarketCard.test.tsx
├── app/
│ └── api/
│ └── markets/
│ ├── route.ts
│ └── route.test.ts # 통합 테스트
└── e2e/
├── markets.spec.ts # E2E 테스트
├── trading.spec.ts
└── auth.spec.ts
외부 서비스 모킹 (Mocking)
Supabase 모킹
jest.mock('@/lib/supabase', () => ({
supabase: {
from: jest.fn(() => ({
select: jest.fn(() => ({
eq: jest.fn(() => Promise.resolve({
data: [{ id: 1, name: 'Test Market' }],
error: null
}))
}))
}))
}
}))
Redis 모킹
jest.mock('@/lib/redis', () => ({
searchMarketsByVector: jest.fn(() => Promise.resolve([
{ slug: 'test-market', similarity_score: 0.95 }
])),
checkRedisHealth: jest.fn(() => Promise.resolve({ connected: true }))
}))
OpenAI 모킹
jest.mock('@/lib/openai', () => ({
generateEmbedding: jest.fn(() => Promise.resolve(
new Array(1536).fill(0.1)
))
}))
테스트 커버리지 검증
커버리지 보고서 실행
npm run test:coverage
커버리지 임계값 설정
{
"jest": {
"coverageThresholds": {
"global": {
"branches": 80,
"functions": 80,
"lines": 80,
"statements": 80
}
}
}
}
피해야 할 일반적인 테스트 실수
실패: 잘못됨: 구현 세부 사항 테스트
expect(component.state.count).toBe(5)
통과: 올바름: 사용자에게 보이는 동작 테스트
expect(screen.getByText('Count: 5')).toBeInTheDocument()
실패: 잘못됨: 깨지기 쉬운 셀렉터
await page.click('.css-class-xyz')
통과: 올바름: 의미론적 셀렉터
await page.click('button:has-text("Submit")')
await page.click('[data-testid="submit-button"]')
실패: 잘못됨: 테스트 격리 미비
test('사용자 생성', () => { })
test('동일 사용자 업데이트', () => { })
통과: 올바름: 독립적인 테스트
test('사용자 생성', () => {
const user = createTestUser()
})
test('사용자 업데이트', () => {
const user = createTestUser()
})
지속적인 테스트
개발 중 Watch 모드
npm test -- --watch
Pre-commit 훅
npm test && npm run lint
CI/CD 통합
- name: Run Tests
run: npm test -- --coverage
- name: Upload Coverage
uses: codecov/codecov-action@v3
모범 사례
- 테스트 먼저 작성 - 항상 TDD를 따르세요.
- 테스트당 하나의 단언(Assert) - 단일 동작에 집중하세요.
- 설명적인 테스트 이름 - 무엇이 테스트되는지 설명하세요.
- 준비-실행-단언(AAA) - 명확한 테스트 구조를 갖추세요.
- 외부 의존성 모킹 - 단위 테스트를 격리하세요.
- 엣지 케이스 테스트 - Null, undefined, 빈 값, 큰 값 등.
- 에러 경로 테스트 - 성공 경로뿐만 아니라 에러 경로도 테스트하세요.
- 테스트 속도 유지 - 단위 테스트는 각각 50ms 미만이어야 합니다.
- 테스트 후 정리 - 부수 효과가 없어야 합니다.
- 커버리지 보고서 검토 - 누락된 부분을 식별하세요.
성공 지표
- 80% 이상의 코드 커버리지 달성
- 모든 테스트 통과 (Green)
- 건너뛰거나 비활성화된 테스트 없음
- 빠른 테스트 실행 (단위 테스트 전체 30초 미만)
- E2E 테스트가 핵심 사용자 흐름을 커버함
- 프로덕션 배포 전에 테스트가 버그를 잡아냄
기억하세요: 테스트는 선택 사항이 아닙니다. 자신감 있는 리팩터링, 신속한 개발 및 프로덕션 안정성을 가능하게 하는 안전망입니다.