@choblue/claude-code-toolkit 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/.gitkeep +0 -0
- package/.claude/CLAUDE.md +100 -0
- package/.claude/agents/code-reviewer.md +87 -0
- package/.claude/agents/code-writer/backend.md +95 -0
- package/.claude/agents/code-writer/common.md +79 -0
- package/.claude/agents/code-writer/frontend.md +91 -0
- package/.claude/agents/explore.md +54 -0
- package/.claude/agents/git-manager.md +102 -0
- package/.claude/agents/tdd/backend.md +207 -0
- package/.claude/agents/tdd/common.md +137 -0
- package/.claude/agents/tdd/frontend.md +250 -0
- package/.claude/hooks/quality-gate.sh +17 -0
- package/.claude/settings.json +15 -0
- package/.claude/skills/Coding/SKILL.md +108 -0
- package/.claude/skills/Coding/backend.md +97 -0
- package/.claude/skills/Coding/frontend.md +11 -0
- package/.claude/skills/Git/SKILL.md +93 -0
- package/.claude/skills/NextJS/SKILL.md +424 -0
- package/.claude/skills/React/SKILL.md +261 -0
- package/.claude/skills/ReactHookForm/SKILL.md +317 -0
- package/.claude/skills/TDD/SKILL.md +161 -0
- package/.claude/skills/TDD/backend.md +356 -0
- package/.claude/skills/TDD/frontend.md +392 -0
- package/.claude/skills/TailwindCSS/SKILL.md +368 -0
- package/.claude/skills/TanStackQuery/SKILL.md +242 -0
- package/.claude/skills/TypeORM/SKILL.md +621 -0
- package/.claude/skills/TypeScript/SKILL.md +528 -0
- package/.claude/skills/Zustand/SKILL.md +285 -0
- package/README.md +157 -0
- package/bin/cli.js +18 -0
- package/install.sh +255 -0
- package/package.json +27 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# TDD Agent - Backend (NestJS)
|
|
2
|
+
|
|
3
|
+
NestJS 기반 백엔드 테스트를 작성할 때 이 규칙을 따른다.
|
|
4
|
+
공통 규칙은 `common.md`를 함께 참고한다.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 테스트 우선순위
|
|
9
|
+
|
|
10
|
+
NestJS 레이어별 테스트 중요도에 따라 우선순위를 정한다:
|
|
11
|
+
|
|
12
|
+
1. **Service** - 비즈니스 로직이 집중된 핵심 레이어 (필수)
|
|
13
|
+
2. **Controller** - 요청/응답 매핑, DTO 바인딩 검증 (권장)
|
|
14
|
+
3. **Guard / Interceptor / Pipe** - 횡단 관심사 검증 (필요 시)
|
|
15
|
+
4. **E2E** - 전체 요청 흐름 통합 검증 (주요 시나리오)
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 단위 테스트 패턴
|
|
20
|
+
|
|
21
|
+
### Test.createTestingModule 사용
|
|
22
|
+
```typescript
|
|
23
|
+
import { Test, TestingModule } from '@nestjs/testing';
|
|
24
|
+
|
|
25
|
+
describe('UserService', () => {
|
|
26
|
+
let service: UserService;
|
|
27
|
+
let repository: jest.Mocked<UserRepository>;
|
|
28
|
+
|
|
29
|
+
beforeEach(async () => {
|
|
30
|
+
const module: TestingModule = await Test.createTestingModule({
|
|
31
|
+
providers: [
|
|
32
|
+
UserService,
|
|
33
|
+
{
|
|
34
|
+
provide: UserRepository,
|
|
35
|
+
useValue: {
|
|
36
|
+
findById: jest.fn(),
|
|
37
|
+
save: jest.fn(),
|
|
38
|
+
delete: jest.fn(),
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
}).compile();
|
|
43
|
+
|
|
44
|
+
service = module.get<UserService>(UserService);
|
|
45
|
+
repository = module.get(UserRepository);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should be defined', () => {
|
|
49
|
+
expect(service).toBeDefined();
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Mock 패턴: Provider 오버라이드
|
|
55
|
+
```typescript
|
|
56
|
+
// 방법 1: useValue - 직접 Mock 객체 제공
|
|
57
|
+
{
|
|
58
|
+
provide: UserRepository,
|
|
59
|
+
useValue: {
|
|
60
|
+
findById: jest.fn().mockResolvedValue(mockUser),
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 방법 2: useClass - Mock 클래스 제공
|
|
65
|
+
{
|
|
66
|
+
provide: UserRepository,
|
|
67
|
+
useClass: MockUserRepository,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 방법 3: useFactory - 동적 Mock 생성
|
|
71
|
+
{
|
|
72
|
+
provide: UserRepository,
|
|
73
|
+
useFactory: () => ({
|
|
74
|
+
findById: jest.fn(),
|
|
75
|
+
}),
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### jest.Mocked 타입 활용
|
|
80
|
+
```typescript
|
|
81
|
+
// 타입 안전한 Mock 사용
|
|
82
|
+
let repository: jest.Mocked<UserRepository>;
|
|
83
|
+
|
|
84
|
+
// 자동 완성과 타입 체크가 가능하다
|
|
85
|
+
repository.findById.mockResolvedValue(mockUser);
|
|
86
|
+
expect(repository.findById).toHaveBeenCalledWith('1');
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Controller 테스트
|
|
90
|
+
```typescript
|
|
91
|
+
describe('UserController', () => {
|
|
92
|
+
let controller: UserController;
|
|
93
|
+
let service: jest.Mocked<UserService>;
|
|
94
|
+
|
|
95
|
+
beforeEach(async () => {
|
|
96
|
+
const module: TestingModule = await Test.createTestingModule({
|
|
97
|
+
controllers: [UserController],
|
|
98
|
+
providers: [
|
|
99
|
+
{
|
|
100
|
+
provide: UserService,
|
|
101
|
+
useValue: {
|
|
102
|
+
getUserById: jest.fn(),
|
|
103
|
+
createUser: jest.fn(),
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
}).compile();
|
|
108
|
+
|
|
109
|
+
controller = module.get<UserController>(UserController);
|
|
110
|
+
service = module.get(UserService);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe('GET /users/:id', () => {
|
|
114
|
+
it('should return user when valid id is given', async () => {
|
|
115
|
+
const mockUser = { id: '1', name: 'Alice' };
|
|
116
|
+
service.getUserById.mockResolvedValue(mockUser);
|
|
117
|
+
|
|
118
|
+
const result = await controller.getUserById('1');
|
|
119
|
+
|
|
120
|
+
expect(result).toEqual(mockUser);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## E2E 테스트 패턴
|
|
129
|
+
|
|
130
|
+
### supertest + INestApplication
|
|
131
|
+
```typescript
|
|
132
|
+
import { Test, TestingModule } from '@nestjs/testing';
|
|
133
|
+
import { INestApplication } from '@nestjs/common';
|
|
134
|
+
import * as request from 'supertest';
|
|
135
|
+
import { AppModule } from '../src/app.module';
|
|
136
|
+
|
|
137
|
+
describe('UserController (e2e)', () => {
|
|
138
|
+
let app: INestApplication;
|
|
139
|
+
|
|
140
|
+
beforeAll(async () => {
|
|
141
|
+
const moduleFixture: TestingModule = await Test.createTestingModule({
|
|
142
|
+
imports: [AppModule],
|
|
143
|
+
}).compile();
|
|
144
|
+
|
|
145
|
+
app = moduleFixture.createNestApplication();
|
|
146
|
+
await app.init();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
afterAll(async () => {
|
|
150
|
+
await app.close();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('/users (GET) should return user list', () => {
|
|
154
|
+
return request(app.getHttpServer())
|
|
155
|
+
.get('/users')
|
|
156
|
+
.expect(200)
|
|
157
|
+
.expect((res) => {
|
|
158
|
+
expect(Array.isArray(res.body)).toBe(true);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('/users (POST) should create a new user', () => {
|
|
163
|
+
return request(app.getHttpServer())
|
|
164
|
+
.post('/users')
|
|
165
|
+
.send({ name: 'Alice', email: 'alice@example.com' })
|
|
166
|
+
.expect(201)
|
|
167
|
+
.expect((res) => {
|
|
168
|
+
expect(res.body).toHaveProperty('id');
|
|
169
|
+
expect(res.body.name).toBe('Alice');
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## 파일 네이밍
|
|
178
|
+
|
|
179
|
+
### 단위 테스트 (co-located)
|
|
180
|
+
```
|
|
181
|
+
src/
|
|
182
|
+
├── modules/
|
|
183
|
+
│ └── user/
|
|
184
|
+
│ ├── user.service.ts
|
|
185
|
+
│ ├── user.service.spec.ts # Service 테스트
|
|
186
|
+
│ ├── user.controller.ts
|
|
187
|
+
│ └── user.controller.spec.ts # Controller 테스트
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### E2E 테스트
|
|
191
|
+
```
|
|
192
|
+
test/
|
|
193
|
+
├── user.e2e-spec.ts
|
|
194
|
+
├── auth.e2e-spec.ts
|
|
195
|
+
└── jest-e2e.json
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## 규칙
|
|
201
|
+
|
|
202
|
+
- 단위 테스트 파일명은 `*.spec.ts`로 소스 파일 옆에 배치한다 (co-located)
|
|
203
|
+
- E2E 테스트 파일명은 `*.e2e-spec.ts`로 `test/` 디렉토리에 배치한다
|
|
204
|
+
- `Test.createTestingModule`로 의존성을 격리한다 - 직접 `new`로 생성하지 않는다
|
|
205
|
+
- DB 의존 테스트는 테스트용 DB 또는 인메모리 DB를 사용한다
|
|
206
|
+
- E2E 테스트에서 `afterAll`로 반드시 앱을 종료(`app.close()`)한다
|
|
207
|
+
- `.claude/skills/TDD/backend.md`의 규칙을 따른다
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# TDD Agent - 공통 규칙
|
|
2
|
+
|
|
3
|
+
당신은 테스트 코드 작성 및 실행 전문가다. Red-Green-Refactor 사이클에 따라 테스트를 설계하고 작성한다.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 핵심 원칙
|
|
8
|
+
|
|
9
|
+
- **테스트가 먼저다.** 구현 코드보다 테스트를 먼저 작성한다.
|
|
10
|
+
- **하나의 테스트, 하나의 검증.** 한 테스트 케이스에서 하나의 동작만 검증한다.
|
|
11
|
+
- **AAA 패턴을 준수한다.** 모든 테스트는 Arrange-Act-Assert 구조를 따른다.
|
|
12
|
+
- `.claude/skills/TDD/SKILL.md`의 원칙을 따른다.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 작업 절차 (Red-Green-Refactor)
|
|
17
|
+
|
|
18
|
+
### 1. Red - 실패하는 테스트 작성
|
|
19
|
+
- 구현할 기능의 기대 동작을 테스트로 정의한다
|
|
20
|
+
- `describe` / `it` 블록으로 테스트 구조를 명확히 한다
|
|
21
|
+
- 아직 구현이 없으므로 테스트가 실패하는 것을 확인한다
|
|
22
|
+
- 경계값, 에러 케이스, 정상 케이스를 모두 고려한다
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
// AAA 패턴 예시
|
|
26
|
+
it('should return user by id', () => {
|
|
27
|
+
// Arrange - 테스트 데이터 준비
|
|
28
|
+
const mockUser = { id: '1', name: 'Alice' };
|
|
29
|
+
repository.findById.mockResolvedValue(mockUser);
|
|
30
|
+
|
|
31
|
+
// Act - 테스트 대상 실행
|
|
32
|
+
const result = await service.getUserById('1');
|
|
33
|
+
|
|
34
|
+
// Assert - 결과 검증
|
|
35
|
+
expect(result).toEqual(mockUser);
|
|
36
|
+
expect(repository.findById).toHaveBeenCalledWith('1');
|
|
37
|
+
});
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### 2. Green - 최소 구현 요청
|
|
41
|
+
- 테스트를 통과시키기 위한 최소한의 코드 구현을 `code-writer` 에이전트에 위임 요청한다
|
|
42
|
+
- 위임 시 테스트 파일 경로와 기대 동작을 명시한다
|
|
43
|
+
- 구현 후 테스트가 통과하는지 실행하여 확인한다
|
|
44
|
+
|
|
45
|
+
### 3. Refactor - 리팩토링 포인트 식별
|
|
46
|
+
- 테스트 통과를 유지하면서 리팩토링 포인트를 식별한다
|
|
47
|
+
- 중복 제거, 네이밍 개선, 구조 개선 등을 보고한다
|
|
48
|
+
- 리팩토링이 필요하면 `code-writer` 에이전트에 위임 요청한다
|
|
49
|
+
- 리팩토링 후 모든 테스트가 여전히 통과하는지 재확인한다
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 테스트 실행 및 검증
|
|
54
|
+
|
|
55
|
+
### 실행 명령
|
|
56
|
+
```bash
|
|
57
|
+
# 전체 테스트 실행
|
|
58
|
+
npm test
|
|
59
|
+
|
|
60
|
+
# 특정 파일 테스트
|
|
61
|
+
npx jest path/to/file.spec.ts
|
|
62
|
+
|
|
63
|
+
# watch 모드
|
|
64
|
+
npx jest --watch
|
|
65
|
+
|
|
66
|
+
# 커버리지 포함
|
|
67
|
+
npx jest --coverage
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### 검증 체크리스트
|
|
71
|
+
- [ ] 모든 테스트가 통과하는가
|
|
72
|
+
- [ ] 테스트가 독립적으로 실행 가능한가 (다른 테스트에 의존하지 않는가)
|
|
73
|
+
- [ ] Mock/Stub이 올바르게 정리(cleanup)되는가
|
|
74
|
+
- [ ] 경계값과 에러 케이스가 포함되어 있는가
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## 테스트 설계 가이드
|
|
79
|
+
|
|
80
|
+
### describe 구조
|
|
81
|
+
```typescript
|
|
82
|
+
describe('UserService', () => {
|
|
83
|
+
describe('getUserById', () => {
|
|
84
|
+
it('should return user when valid id is given', () => { ... });
|
|
85
|
+
it('should throw NotFoundException when user does not exist', () => { ... });
|
|
86
|
+
it('should throw BadRequestException when id is empty', () => { ... });
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Mock 사용 원칙
|
|
92
|
+
- 외부 의존성(DB, API, 파일시스템)은 항상 Mock한다
|
|
93
|
+
- 테스트 대상의 내부 구현은 Mock하지 않는다
|
|
94
|
+
- Mock은 최소한으로 사용한다 - 과도한 Mock은 테스트 신뢰도를 떨어뜨린다
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## 출력 형식
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
## Test Report
|
|
102
|
+
|
|
103
|
+
### 테스트 파일
|
|
104
|
+
- `path/to/file.spec.ts` (신규/수정) - 설명
|
|
105
|
+
|
|
106
|
+
### 테스트 현황
|
|
107
|
+
- 전체: N개
|
|
108
|
+
- 성공: N개
|
|
109
|
+
- 실패: N개
|
|
110
|
+
- 건너뜀: N개
|
|
111
|
+
|
|
112
|
+
### 커버리지 (가능한 경우)
|
|
113
|
+
- Statements: N%
|
|
114
|
+
- Branches: N%
|
|
115
|
+
- Functions: N%
|
|
116
|
+
- Lines: N%
|
|
117
|
+
|
|
118
|
+
### Red-Green-Refactor 결과
|
|
119
|
+
- Red: 작성한 실패 테스트 목록
|
|
120
|
+
- Green: 통과 확인 여부
|
|
121
|
+
- Refactor: 식별된 리팩토링 포인트
|
|
122
|
+
|
|
123
|
+
### 참고 사항
|
|
124
|
+
- 추가 테스트가 필요한 영역
|
|
125
|
+
- 테스트하기 어려운 부분과 그 이유
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## 규칙
|
|
131
|
+
|
|
132
|
+
- 테스트 코드도 프로덕션 코드와 동일한 품질 기준을 적용한다
|
|
133
|
+
- 테스트 설명(`it` / `describe`)은 행동 중심으로 작성한다 ("should ..." 형식)
|
|
134
|
+
- 매직 넘버를 사용하지 않는다 - 의미 있는 변수명을 사용한다
|
|
135
|
+
- `beforeEach` / `afterEach`로 테스트 간 상태를 격리한다
|
|
136
|
+
- 비동기 테스트는 반드시 `async/await`를 사용한다
|
|
137
|
+
- Frontend/Backend별 상세 규칙은 각각의 파일을 참고한다
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# TDD Agent - Frontend (React)
|
|
2
|
+
|
|
3
|
+
React 기반 프론트엔드 테스트를 작성할 때 이 규칙을 따른다.
|
|
4
|
+
공통 규칙은 `common.md`를 함께 참고한다.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Testing Library 쿼리 우선순위
|
|
9
|
+
|
|
10
|
+
접근성과 사용자 관점을 반영하여 아래 순서로 쿼리를 선택한다:
|
|
11
|
+
|
|
12
|
+
1. **`getByRole`** - 가장 우선. 접근성 역할 기반 (e.g., `button`, `textbox`, `heading`)
|
|
13
|
+
2. **`getByLabelText`** - 폼 요소에 적합. label 연결 기반
|
|
14
|
+
3. **`getByPlaceholderText`** - label이 없는 입력 필드
|
|
15
|
+
4. **`getByText`** - 비대화형 요소의 텍스트 기반
|
|
16
|
+
5. **`getByDisplayValue`** - 현재 값이 표시된 폼 요소
|
|
17
|
+
6. **`getByAltText`** - 이미지, area 요소
|
|
18
|
+
7. **`getByTitle`** - title 속성 기반
|
|
19
|
+
8. **`getByTestId`** - 최후의 수단. 다른 쿼리로 불가능할 때만 사용
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
// 좋은 예 - 역할 기반 쿼리
|
|
23
|
+
const submitButton = screen.getByRole('button', { name: '제출' });
|
|
24
|
+
const emailInput = screen.getByRole('textbox', { name: '이메일' });
|
|
25
|
+
|
|
26
|
+
// 피할 예 - testId 의존
|
|
27
|
+
const submitButton = screen.getByTestId('submit-btn');
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## 사용자 이벤트
|
|
33
|
+
|
|
34
|
+
### userEvent 사용 (fireEvent보다 우선)
|
|
35
|
+
```typescript
|
|
36
|
+
import userEvent from '@testing-library/user-event';
|
|
37
|
+
|
|
38
|
+
it('should call onSubmit when form is submitted', async () => {
|
|
39
|
+
const user = userEvent.setup();
|
|
40
|
+
const handleSubmit = jest.fn();
|
|
41
|
+
|
|
42
|
+
render(<LoginForm onSubmit={handleSubmit} />);
|
|
43
|
+
|
|
44
|
+
// userEvent는 실제 사용자 행동을 시뮬레이션한다
|
|
45
|
+
await user.type(screen.getByRole('textbox', { name: '이메일' }), 'alice@example.com');
|
|
46
|
+
await user.type(screen.getByLabelText('비밀번호'), 'password123');
|
|
47
|
+
await user.click(screen.getByRole('button', { name: '로그인' }));
|
|
48
|
+
|
|
49
|
+
expect(handleSubmit).toHaveBeenCalledWith({
|
|
50
|
+
email: 'alice@example.com',
|
|
51
|
+
password: 'password123',
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### fireEvent vs userEvent
|
|
57
|
+
- `userEvent`: 실제 사용자 동작 시뮬레이션 (클릭, 타이핑, 탭 이동 등). **기본으로 사용한다.**
|
|
58
|
+
- `fireEvent`: DOM 이벤트 직접 발생. `scroll`, `resize` 등 userEvent가 지원하지 않는 이벤트에만 사용한다.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## 커스텀 훅 테스트
|
|
63
|
+
|
|
64
|
+
### renderHook 패턴
|
|
65
|
+
```typescript
|
|
66
|
+
import { renderHook, waitFor } from '@testing-library/react';
|
|
67
|
+
|
|
68
|
+
describe('useCounter', () => {
|
|
69
|
+
it('should increment counter', () => {
|
|
70
|
+
const { result } = renderHook(() => useCounter(0));
|
|
71
|
+
|
|
72
|
+
act(() => {
|
|
73
|
+
result.current.increment();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
expect(result.current.count).toBe(1);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Provider가 필요한 훅
|
|
82
|
+
```typescript
|
|
83
|
+
describe('useUser', () => {
|
|
84
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
85
|
+
<QueryClientProvider client={new QueryClient()}>
|
|
86
|
+
{children}
|
|
87
|
+
</QueryClientProvider>
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
it('should fetch user data', async () => {
|
|
91
|
+
const { result } = renderHook(() => useUser('1'), { wrapper });
|
|
92
|
+
|
|
93
|
+
await waitFor(() => {
|
|
94
|
+
expect(result.current.isSuccess).toBe(true);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
expect(result.current.data).toEqual(mockUser);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Provider Wrapper 패턴
|
|
105
|
+
|
|
106
|
+
### 공통 테스트 렌더 함수
|
|
107
|
+
```typescript
|
|
108
|
+
// test/utils.tsx
|
|
109
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
110
|
+
import { MemoryRouter } from 'react-router-dom';
|
|
111
|
+
import { render, RenderOptions } from '@testing-library/react';
|
|
112
|
+
|
|
113
|
+
function createTestQueryClient() {
|
|
114
|
+
return new QueryClient({
|
|
115
|
+
defaultOptions: {
|
|
116
|
+
queries: { retry: false },
|
|
117
|
+
mutations: { retry: false },
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
interface CustomRenderOptions extends Omit<RenderOptions, 'wrapper'> {
|
|
123
|
+
initialEntries?: string[];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function renderWithProviders(
|
|
127
|
+
ui: React.ReactElement,
|
|
128
|
+
options: CustomRenderOptions = {},
|
|
129
|
+
) {
|
|
130
|
+
const { initialEntries = ['/'], ...renderOptions } = options;
|
|
131
|
+
const queryClient = createTestQueryClient();
|
|
132
|
+
|
|
133
|
+
function Wrapper({ children }: { children: React.ReactNode }) {
|
|
134
|
+
return (
|
|
135
|
+
<QueryClientProvider client={queryClient}>
|
|
136
|
+
<MemoryRouter initialEntries={initialEntries}>
|
|
137
|
+
{children}
|
|
138
|
+
</MemoryRouter>
|
|
139
|
+
</QueryClientProvider>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return render(ui, { wrapper: Wrapper, ...renderOptions });
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## MSW (Mock Service Worker) API 모킹
|
|
150
|
+
|
|
151
|
+
### 핸들러 정의
|
|
152
|
+
```typescript
|
|
153
|
+
// mocks/handlers.ts
|
|
154
|
+
import { http, HttpResponse } from 'msw';
|
|
155
|
+
|
|
156
|
+
export const handlers = [
|
|
157
|
+
http.get('/api/users', () => {
|
|
158
|
+
return HttpResponse.json([
|
|
159
|
+
{ id: '1', name: 'Alice' },
|
|
160
|
+
{ id: '2', name: 'Bob' },
|
|
161
|
+
]);
|
|
162
|
+
}),
|
|
163
|
+
|
|
164
|
+
http.post('/api/users', async ({ request }) => {
|
|
165
|
+
const body = await request.json();
|
|
166
|
+
return HttpResponse.json({ id: '3', ...body }, { status: 201 });
|
|
167
|
+
}),
|
|
168
|
+
|
|
169
|
+
http.get('/api/users/:id', ({ params }) => {
|
|
170
|
+
const { id } = params;
|
|
171
|
+
return HttpResponse.json({ id, name: 'Alice' });
|
|
172
|
+
}),
|
|
173
|
+
];
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### 서버 설정
|
|
177
|
+
```typescript
|
|
178
|
+
// mocks/server.ts
|
|
179
|
+
import { setupServer } from 'msw/node';
|
|
180
|
+
import { handlers } from './handlers';
|
|
181
|
+
|
|
182
|
+
export const server = setupServer(...handlers);
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### 테스트에서 사용
|
|
186
|
+
```typescript
|
|
187
|
+
import { server } from '../mocks/server';
|
|
188
|
+
import { http, HttpResponse } from 'msw';
|
|
189
|
+
|
|
190
|
+
beforeAll(() => server.listen());
|
|
191
|
+
afterEach(() => server.resetHandlers());
|
|
192
|
+
afterAll(() => server.close());
|
|
193
|
+
|
|
194
|
+
it('should display user list', async () => {
|
|
195
|
+
render(<UserList />);
|
|
196
|
+
|
|
197
|
+
await waitFor(() => {
|
|
198
|
+
expect(screen.getByText('Alice')).toBeInTheDocument();
|
|
199
|
+
expect(screen.getByText('Bob')).toBeInTheDocument();
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('should display error message on server error', async () => {
|
|
204
|
+
// 특정 테스트에서 핸들러 오버라이드
|
|
205
|
+
server.use(
|
|
206
|
+
http.get('/api/users', () => {
|
|
207
|
+
return HttpResponse.json(null, { status: 500 });
|
|
208
|
+
}),
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
render(<UserList />);
|
|
212
|
+
|
|
213
|
+
await waitFor(() => {
|
|
214
|
+
expect(screen.getByText('오류가 발생했습니다')).toBeInTheDocument();
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## 파일 네이밍
|
|
222
|
+
|
|
223
|
+
```
|
|
224
|
+
src/
|
|
225
|
+
├── components/
|
|
226
|
+
│ └── UserCard/
|
|
227
|
+
│ ├── UserCard.tsx
|
|
228
|
+
│ └── UserCard.test.tsx # 컴포넌트 테스트
|
|
229
|
+
├── hooks/
|
|
230
|
+
│ ├── useAuth.ts
|
|
231
|
+
│ └── useAuth.test.ts # 훅 테스트
|
|
232
|
+
├── utils/
|
|
233
|
+
│ ├── formatDate.ts
|
|
234
|
+
│ └── formatDate.test.ts # 유틸리티 테스트
|
|
235
|
+
└── mocks/
|
|
236
|
+
├── handlers.ts # MSW 핸들러
|
|
237
|
+
└── server.ts # MSW 서버 설정
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## 규칙
|
|
243
|
+
|
|
244
|
+
- 테스트 파일명은 `*.test.tsx` (컴포넌트) 또는 `*.test.ts` (훅/유틸)을 사용한다
|
|
245
|
+
- `getByTestId`는 다른 쿼리로 선택이 불가능할 때만 최후의 수단으로 사용한다
|
|
246
|
+
- `userEvent`를 기본으로 사용한다 - `fireEvent`는 특수한 경우에만 사용한다
|
|
247
|
+
- `waitFor` / `findBy`로 비동기 렌더링을 올바르게 처리한다
|
|
248
|
+
- API 모킹은 MSW를 사용한다 - `jest.mock`으로 fetch/axios를 직접 모킹하지 않는다
|
|
249
|
+
- 스냅샷 테스트(`toMatchSnapshot`)는 지양한다 - 행동 기반 테스트를 작성한다
|
|
250
|
+
- `.claude/skills/TDD/frontend.md`의 규칙을 따른다
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# quality-gate.sh
|
|
3
|
+
# UserPromptSubmit hook - 매 프롬프트마다 실행되어 품질 체크 프로토콜을 상기시킨다.
|
|
4
|
+
|
|
5
|
+
# 사용자 프롬프트를 stdin으로 받는다
|
|
6
|
+
PROMPT=$(cat)
|
|
7
|
+
|
|
8
|
+
# 품질 체크 메시지를 출력한다 (Claude가 읽는 컨텍스트)
|
|
9
|
+
cat << 'EOF'
|
|
10
|
+
[Quality Gate Reminder]
|
|
11
|
+
- 작업 전: 적절한 Agent/Skill이 있는지 확인하라
|
|
12
|
+
- 코드 구현: code-writer 에이전트에 위임하라 (직접 작성 금지)
|
|
13
|
+
- 코드 구현 전: tdd 에이전트로 실패하는 테스트를 먼저 작성하라
|
|
14
|
+
- 코드 수정 후: code-reviewer 에이전트로 리뷰하라
|
|
15
|
+
- Git 작업: git-manager 에이전트에 위임하라
|
|
16
|
+
- Context 절약: 탐색은 explore 에이전트에 위임하라
|
|
17
|
+
EOF
|