ワンクリックで
write-test
React 단위 테스트(Vitest) 및 E2E 테스트(Playwright) 작성 컨벤션과 Mocking 가이드
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
メニュー
React 단위 테스트(Vitest) 및 E2E 테스트(Playwright) 작성 컨벤션과 Mocking 가이드
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
SOC 職業分類に基づく
프로젝트 구조와 네이밍 컨벤션에 맞게 새 React 컴포넌트를 추가하는 가이드
PDF(인쇄 레이아웃) 및 Excel(xlsx 동적 로딩) 파일 내보내기 구현 및 갱신 패턴 가이드
Gemini 3.1 Flash Lite API를 사용한 계기판 OCR 및 증빙 서류 AI 판별 연동 패턴 가이드
Sentry에 잡힌 환경/브라우저/외부 의존성 발 노이즈 에러를 필터링한다. 사용자가 "Sentry 노이즈", "Sentry 에러 필터", "이 에러 무시", "Sentry에 자꾸 뜨는 X 막아줘" 등을 요청할 때 발동한다.
Cloud Functions, 프론트엔드 빌드 및 배포 시 발생하는 다양한 오류(Node 버전, 의존성 충돌, 소스맵)의 해결 패턴을 모아둔 가이드.
functions/src/ 디렉터리에 새 Cloud Function을 추가하고 index.ts에 등록하는 패턴 가이드
| name | write-test |
| description | React 단위 테스트(Vitest) 및 E2E 테스트(Playwright) 작성 컨벤션과 Mocking 가이드 |
src/__tests__/ ← 프론트엔드 단위 테스트 (Vitest + jsdom)
├── setup.ts ← @testing-library/jest-dom import
├── hooks/ ← 커스텀 훅 테스트 (22개)
├── lib/ ← 유틸/라이브러리 테스트 (16개)
└── components/ ← 컴포넌트 렌더링 테스트
functions/src/__tests__/ ← Cloud Functions 테스트 (별도 vitest 설정)
├── emulator.setup.ts ← Firebase Emulator 초기화
├── *.test.ts ← 순수 단위 테스트
└── *.emulator.test.ts ← Emulator 연동 테스트
e2e/ ← E2E 테스트 (Playwright)
└── *.spec.ts ← 12개 시나리오
| 대상 | 위치 | 파일명 |
|---|---|---|
| 커스텀 훅 | src/__tests__/hooks/ | useXxx.test.ts |
| lib 유틸 | src/__tests__/lib/ | 모듈명.test.ts |
| 컴포넌트 | src/__tests__/components/ | ComponentName.test.tsx |
| Cloud Function | functions/src/__tests__/ | 함수명.test.ts |
⚠️ 소스 파일 옆에
.test.ts를 두지 않는다.__tests__/디렉터리에 통합 관리.
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { renderHook, act } from '@testing-library/react';
// External dependency mock — import보다 위에 선언
const mockShowToast = vi.fn();
vi.mock('../../hooks/useToast', () => ({
useToast: () => ({ showToast: mockShowToast }),
}));
// Hook import는 vi.mock 뒤에
import useMyHook from '../../hooks/useMyHook';
describe('useMyHook', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('정상 케이스를 설명한다', async () => {
const { result } = renderHook(() => useMyHook());
await act(async () => {
await result.current.doSomething();
});
expect(result.current.state).toBe('expected');
});
});
Firebase 모듈은 항상 mock 처리한다:
vi.mock('../../lib/firebase', () => ({
db: {},
auth: { currentUser: { uid: 'test-uid' } },
}));
// Vitest 로컬 실행 시 auth/invalid-api-key 에러를 방지하려면,
// 테스트 상단에 VITE_FIREBASE_API_KEY 환경변수나 Mock 초기화 코드가 필요할 수 있다.
vi.mock('firebase/firestore', () => ({
collection: vi.fn(),
doc: vi.fn(),
getDocs: vi.fn(),
query: vi.fn(),
where: vi.fn(),
orderBy: vi.fn(),
Timestamp: {
now: () => ({ toDate: () => new Date() }),
fromDate: (d: Date) => ({ toDate: () => d }),
},
}));
act(...) 래핑@testing-library/react의 act(...) 로 감싸야 한다.Warning: An update to X inside a test was not wrapped in act(...) 경고가 발생하며, 비동기 상태의 단언(assertion)이 실패할 수 있다.Hook의 useEffect 의존성에 Mock 반환값이 포함되면, 매 렌더마다 새 객체가 생성되어 무한 루프가 발생한다.
// ❌ BAD — 호출할 때마다 새 객체 생성 → 무한 렌더
vi.mock('../../hooks/useToast', () => ({
useToast: () => ({ showToast: vi.fn() }), // 매번 새 vi.fn()
}));
// ✅ GOOD — 외부에서 한 번만 생성 → 안정적 참조
const mockShowToast = vi.fn();
vi.mock('../../hooks/useToast', () => ({
useToast: () => ({ showToast: mockShowToast }),
}));
증상: 테스트가 5000ms 타임아웃으로 실패.
act()경고가 대량 출력.
Hook 내부의 useEffect가 호출하는 비동기 함수가 mock되지 않으면, Promise가 영원히 미결(pending) 상태로 남는다.
// ❌ BAD — getVehicleEndKmBefore가 mock에 없음 → Promise 미결 → 타임아웃
vi.mock('../../lib/firestore', () => ({
getVehicles: vi.fn(),
getLastVehicleEndKm: vi.fn(),
// getVehicleEndKmBefore 누락!
}));
// ✅ GOOD — useEffect가 호출할 수 있는 모든 함수를 mock
vi.mock('../../lib/firestore', () => ({
getVehicles: vi.fn(),
getLastVehicleEndKm: vi.fn(),
getVehicleEndKmBefore: vi.fn().mockResolvedValue(null),
}));
증상: 특정 테스트만 타임아웃. 로직이 단순해 보이는데 왜 느린지 이해가 안 됨.
useAuth()의 user 객체처럼 넓은 객체를 의존성 배열에 넣으면, 메타데이터 변경(displayName 등)만으로도 effect가 재실행된다.
// ❌ BAD — user 전체를 의존성에 → 불필요한 재실행
useEffect(() => {
fetchData(user.orgId);
}, [user]); // user.displayName 변경에도 트리거
// ✅ GOOD — 필요한 값만 추출
useEffect(() => {
fetchData(user.orgId);
}, [user.orgId]); // orgId가 바뀔 때만 트리거
부득이하게 넓은 객체를 써야 하면
// eslint-disable-next-line react-hooks/exhaustive-deps주석을 추가하되, stale closure 위험이 없는지 반드시 검증 후 사용한다.
it('성공 시 결과를 반환한다'))'XXX 에러 시 토스트를 표시한다'*.test.ts)Firebase Admin을 mock하여 네트워크 없이 실행:
vi.mock('firebase-admin/firestore', () => ({
getFirestore: () => mockDb,
}));
*.emulator.test.ts)실제 Emulator에 데이터를 넣고 검증:
import { setup, teardown } from './emulator.setup';
beforeAll(async () => { await setup(); });
afterAll(async () => { await teardown(); });
Emulator 테스트는 CI에서 실행 시간이 길므로, 핵심 비즈니스 로직에만 사용.
e2e/ 디렉터리에 기능명.spec.ts로 생성.
page.waitForSelector 대신 expect(locator).toBeVisible() 사용test.fixme()로 마킹import { test, expect } from '@playwright/test';
test('랜딩 페이지가 정상 로드된다', async ({ page }) => {
await page.goto('/');
await expect(page.locator('h1')).toBeVisible();
await expect(page).toHaveTitle(/차량운행일지/);
});
| 상황 | 테스트 여부 |
|---|---|
| 비즈니스 로직이 있는 Hook | ✅ 필수 |
| 유틸/헬퍼 함수 | ✅ 필수 |
| Cloud Function (onCall, trigger) | ✅ 필수 |
| 순수 UI 컴포넌트 (표시만) | ❌ 불필요 |
| 라우팅/레이아웃 변경 | ❌ 불필요 |
npm run test # 프론트엔드 단위 테스트
npm run test:e2e # E2E 테스트
전체 테스트 스위트 실행은
/test워크플로우를 사용한다.