Frontend TDD (Test-Driven Development) 스킬
$ARGUMENTS 기능을 TDD 방식으로 구현합니다.
작업 디렉토리
모든 작업은 blog-frontend/ 디렉토리에서 수행합니다.
TDD 사이클 (Red-Green-Refactor)
반드시 아래 순서를 따라야 합니다:
1. Red: 실패하는 테스트 작성
- •구현하려는 기능의 테스트를 먼저 작성합니다
- •테스트 파일 위치: 컴포넌트와 같은 폴더에
*.test.tsx파일 - •React Testing Library + Vitest 사용
- •테스트 실행하여 실패 확인 (이 단계에서 실패해야 정상)
bash
cd blog-frontend && npm test -- --run
2. Green: 최소한의 코드로 테스트 통과
- •테스트를 통과시키기 위한 최소한의 코드만 작성
- •완벽한 코드가 아니어도 됨
- •테스트 실행하여 성공 확인
3. Refactor: 코드 개선
- •테스트가 통과하는 상태를 유지하면서 코드 개선
- •중복 제거, 네이밍 개선, 구조 개선
- •리팩토링 후 테스트 재실행하여 여전히 성공 확인
필수 규칙
- •
테스트 없이 프로덕션 코드 작성 금지
- •모든 새로운 기능은 테스트가 먼저 존재해야 함
- •
한 번에 하나의 테스트만
- •여러 테스트를 한꺼번에 작성하지 않음
- •하나의 테스트 → 구현 → 다음 테스트
- •
테스트 실행 필수
- •각 단계에서 반드시 테스트를 실행하고 결과 확인
- •Red 단계: 실패 확인
- •Green/Refactor 단계: 성공 확인
- •
작은 단위로 진행
- •큰 기능은 작은 테스트 케이스로 분해
- •점진적으로 기능 완성
테스트 작성 가이드
컴포넌트 테스트 예시
tsx
// src/components/Button/Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import { Button } from './Button';
describe('Button', () => {
it('renders with text', () => {
render(<Button>Click me</Button>);
expect(screen.getByRole('button', { name: /click me/i })).toBeInTheDocument();
});
it('calls onClick when clicked', () => {
const handleClick = vi.fn();
render(<Button onClick={handleClick}>Click</Button>);
fireEvent.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
});
커스텀 훅 테스트 예시
tsx
// src/hooks/useCounter.test.ts
import { renderHook, act } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import { useCounter } from './useCounter';
describe('useCounter', () => {
it('should initialize with default value', () => {
const { result } = renderHook(() => useCounter());
expect(result.current.count).toBe(0);
});
it('should increment counter', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
});
비동기 컴포넌트 테스트 예시
tsx
// src/features/posts/PostList.test.tsx
import { render, screen, waitFor } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import { PostList } from './PostList';
describe('PostList', () => {
it('displays loading state initially', () => {
render(<PostList />);
expect(screen.getByText(/loading/i)).toBeInTheDocument();
});
it('displays posts after fetch', async () => {
render(<PostList />);
await waitFor(() => {
expect(screen.getByText('Post Title')).toBeInTheDocument();
});
});
});
테스트 쿼리 우선순위
React Testing Library의 쿼리 우선순위를 따릅니다:
- •getByRole - 접근성 역할 기반 (최우선)
- •getByLabelText - 폼 요소의 라벨
- •getByPlaceholderText - placeholder
- •getByText - 텍스트 콘텐츠
- •getByTestId - 최후의 수단
tsx
// Good - 역할 기반 쿼리
screen.getByRole('button', { name: /submit/i });
screen.getByRole('textbox', { name: /email/i });
// Avoid - testId는 최후의 수단
screen.getByTestId('submit-button');
파일 구조
테스트 파일은 테스트 대상과 같은 폴더에 배치합니다:
code
src/components/Button/ ├── index.ts ├── Button.tsx ├── Button.test.tsx # 테스트 파일 └── Button.module.css
주의사항
1. <style jsx> 문법 미지원
Next.js에서 <style jsx> 문법은 기본 지원되지 않습니다. CSS 애니메이션은 globals.css에 정의하고 Tailwind arbitrary values를 사용하세요:
css
/* globals.css */
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
tsx
// Tailwind arbitrary values로 사용 <div className="animate-[fadeIn_0.6s_ease-out]">...</div>
2. API 서비스 테스트 시 fetch 모킹
typescript
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
describe("postApi", () => {
const mockFetch = vi.fn();
beforeEach(() => {
vi.stubGlobal("fetch", mockFetch);
});
afterEach(() => {
vi.unstubAllGlobals();
});
it("API 호출 테스트", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => ({ id: 1, title: "제목" }),
});
const result = await createPost({ title: "제목", content: "내용", authorId: 1 });
expect(mockFetch).toHaveBeenCalledWith("/api/posts", expect.objectContaining({
method: "POST",
}));
});
});
진행 보고
각 TDD 사이클마다 다음을 보고합니다:
- •현재 단계 (Red/Green/Refactor)
- •테스트 실행 결과
- •다음 단계 계획