@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,356 @@
|
|
|
1
|
+
# TDD Skill - Backend (NestJS)
|
|
2
|
+
|
|
3
|
+
NestJS 백엔드 테스트에 적용되는 규칙이다.
|
|
4
|
+
공통 원칙은 `SKILL.md`를 함께 참고한다.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 1. 레이어별 테스트 전략
|
|
9
|
+
|
|
10
|
+
### 우선순위
|
|
11
|
+
|
|
12
|
+
테스트 작성 우선순위는 비즈니스 로직이 집중된 레이어 순서로 한다.
|
|
13
|
+
|
|
14
|
+
| 우선순위 | 레이어 | 이유 |
|
|
15
|
+
|----------|--------|------|
|
|
16
|
+
| 1 | **Service** | 비즈니스 로직의 핵심. 가장 많은 분기와 로직이 존재한다 |
|
|
17
|
+
| 2 | **Controller** | 요청/응답 변환, DTO 유효성 검증 확인 |
|
|
18
|
+
| 3 | **Guard / Pipe** | 인증/인가, 데이터 변환 등 횡단 관심사 |
|
|
19
|
+
| 4 | **Repository** | 복잡한 쿼리가 있는 경우에 한정 |
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 2. NestJS Testing Module
|
|
24
|
+
|
|
25
|
+
### 기본 설정
|
|
26
|
+
|
|
27
|
+
NestJS는 `@nestjs/testing`을 사용하여 DI 컨테이너를 구성한다.
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import { Test, TestingModule } from '@nestjs/testing';
|
|
31
|
+
import { UserService } from './user.service';
|
|
32
|
+
import { UserRepository } from './user.repository';
|
|
33
|
+
|
|
34
|
+
describe('UserService', () => {
|
|
35
|
+
let service: UserService;
|
|
36
|
+
let repository: jest.Mocked<UserRepository>;
|
|
37
|
+
|
|
38
|
+
beforeEach(async () => {
|
|
39
|
+
const module: TestingModule = await Test.createTestingModule({
|
|
40
|
+
providers: [
|
|
41
|
+
UserService,
|
|
42
|
+
{
|
|
43
|
+
provide: UserRepository,
|
|
44
|
+
useValue: {
|
|
45
|
+
findById: jest.fn(),
|
|
46
|
+
save: jest.fn(),
|
|
47
|
+
delete: jest.fn(),
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
}).compile();
|
|
52
|
+
|
|
53
|
+
service = module.get<UserService>(UserService);
|
|
54
|
+
repository = module.get(UserRepository);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
afterEach(() => {
|
|
58
|
+
jest.clearAllMocks();
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### 규칙
|
|
64
|
+
- 테스트 대상 클래스는 실제 구현체를 사용한다
|
|
65
|
+
- 의존성(DI로 주입되는 것)은 Mock으로 대체한다
|
|
66
|
+
- `afterEach`에서 `jest.clearAllMocks()`를 호출하여 테스트 간 격리를 보장한다
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## 3. DI Mock 패턴
|
|
71
|
+
|
|
72
|
+
### Provider 오버라이드
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
// 방법 1: useValue - 필요한 메서드만 Mock 객체로 제공
|
|
76
|
+
{
|
|
77
|
+
provide: UserRepository,
|
|
78
|
+
useValue: {
|
|
79
|
+
findById: jest.fn(),
|
|
80
|
+
save: jest.fn(),
|
|
81
|
+
},
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 방법 2: useClass - Mock 클래스를 정의하여 제공
|
|
85
|
+
{
|
|
86
|
+
provide: UserRepository,
|
|
87
|
+
useClass: MockUserRepository,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 방법 3: useFactory - 동적으로 Mock 생성
|
|
91
|
+
{
|
|
92
|
+
provide: UserRepository,
|
|
93
|
+
useFactory: () => ({
|
|
94
|
+
findById: jest.fn().mockResolvedValue(mockUser),
|
|
95
|
+
save: jest.fn().mockResolvedValue(mockUser),
|
|
96
|
+
}),
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Token 기반 주입 Mock
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
// 인터페이스 기반 주입인 경우
|
|
104
|
+
{
|
|
105
|
+
provide: 'USER_REPOSITORY',
|
|
106
|
+
useValue: {
|
|
107
|
+
findById: jest.fn(),
|
|
108
|
+
},
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ConfigService Mock
|
|
112
|
+
{
|
|
113
|
+
provide: ConfigService,
|
|
114
|
+
useValue: {
|
|
115
|
+
get: jest.fn((key: string) => {
|
|
116
|
+
const config = { JWT_SECRET: 'test-secret', DB_HOST: 'localhost' };
|
|
117
|
+
return config[key];
|
|
118
|
+
}),
|
|
119
|
+
},
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## 4. Service 테스트
|
|
126
|
+
|
|
127
|
+
### 비즈니스 로직 테스트
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
describe('UserService', () => {
|
|
131
|
+
describe('findById', () => {
|
|
132
|
+
it('should return user when user exists', async () => {
|
|
133
|
+
// Arrange
|
|
134
|
+
const mockUser = { id: '1', name: 'John', email: 'john@test.com' };
|
|
135
|
+
repository.findById.mockResolvedValue(mockUser);
|
|
136
|
+
|
|
137
|
+
// Act
|
|
138
|
+
const result = await service.findById('1');
|
|
139
|
+
|
|
140
|
+
// Assert
|
|
141
|
+
expect(result).toEqual(mockUser);
|
|
142
|
+
expect(repository.findById).toHaveBeenCalledWith('1');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should throw UserNotFoundException when user does not exist', async () => {
|
|
146
|
+
// Arrange
|
|
147
|
+
repository.findById.mockResolvedValue(null);
|
|
148
|
+
|
|
149
|
+
// Act & Assert
|
|
150
|
+
await expect(service.findById('999')).rejects.toThrow(
|
|
151
|
+
UserNotFoundException,
|
|
152
|
+
);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## 5. DTO 검증 테스트
|
|
161
|
+
|
|
162
|
+
`class-validator`와 `ValidationPipe`를 활용한 DTO 유효성 검증을 테스트한다.
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
import { validate } from 'class-validator';
|
|
166
|
+
import { plainToInstance } from 'class-transformer';
|
|
167
|
+
import { CreateUserDto } from './create-user.dto';
|
|
168
|
+
|
|
169
|
+
describe('CreateUserDto', () => {
|
|
170
|
+
it('should pass validation with valid data', async () => {
|
|
171
|
+
// Arrange
|
|
172
|
+
const dto = plainToInstance(CreateUserDto, {
|
|
173
|
+
name: 'John',
|
|
174
|
+
email: 'john@test.com',
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// Act
|
|
178
|
+
const errors = await validate(dto);
|
|
179
|
+
|
|
180
|
+
// Assert
|
|
181
|
+
expect(errors).toHaveLength(0);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('should fail validation when email is invalid', async () => {
|
|
185
|
+
// Arrange
|
|
186
|
+
const dto = plainToInstance(CreateUserDto, {
|
|
187
|
+
name: 'John',
|
|
188
|
+
email: 'invalid-email',
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// Act
|
|
192
|
+
const errors = await validate(dto);
|
|
193
|
+
|
|
194
|
+
// Assert
|
|
195
|
+
expect(errors).toHaveLength(1);
|
|
196
|
+
expect(errors[0].property).toBe('email');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('should fail validation when name is empty', async () => {
|
|
200
|
+
// Arrange
|
|
201
|
+
const dto = plainToInstance(CreateUserDto, {
|
|
202
|
+
name: '',
|
|
203
|
+
email: 'john@test.com',
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Act
|
|
207
|
+
const errors = await validate(dto);
|
|
208
|
+
|
|
209
|
+
// Assert
|
|
210
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
211
|
+
expect(errors[0].property).toBe('name');
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## 6. 비동기 테스트 패턴
|
|
219
|
+
|
|
220
|
+
### async/await
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
// 성공 케이스
|
|
224
|
+
it('should create user successfully', async () => {
|
|
225
|
+
repository.save.mockResolvedValue(mockUser);
|
|
226
|
+
|
|
227
|
+
const result = await service.createUser(createUserDto);
|
|
228
|
+
|
|
229
|
+
expect(result).toEqual(mockUser);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// 에러 케이스
|
|
233
|
+
it('should throw error when save fails', async () => {
|
|
234
|
+
repository.save.mockRejectedValue(new Error('DB connection failed'));
|
|
235
|
+
|
|
236
|
+
await expect(service.createUser(createUserDto)).rejects.toThrow(
|
|
237
|
+
'DB connection failed',
|
|
238
|
+
);
|
|
239
|
+
});
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Promise 체이닝 (레거시 코드 테스트 시)
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
it('should resolve with user data', () => {
|
|
246
|
+
repository.findById.mockResolvedValue(mockUser);
|
|
247
|
+
|
|
248
|
+
return service.findById('1').then((result) => {
|
|
249
|
+
expect(result).toEqual(mockUser);
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### 규칙
|
|
255
|
+
- `async/await`를 기본으로 사용한다
|
|
256
|
+
- 비동기 테스트에서 `await`를 빠뜨리지 않는다 (테스트가 항상 통과하는 위험)
|
|
257
|
+
- 에러 케이스는 `rejects.toThrow()`로 검증한다
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
## 7. E2E 테스트
|
|
262
|
+
|
|
263
|
+
### 기본 설정
|
|
264
|
+
|
|
265
|
+
```typescript
|
|
266
|
+
import { Test, TestingModule } from '@nestjs/testing';
|
|
267
|
+
import { INestApplication, ValidationPipe } from '@nestjs/common';
|
|
268
|
+
import * as request from 'supertest';
|
|
269
|
+
import { AppModule } from '../src/app.module';
|
|
270
|
+
|
|
271
|
+
describe('UserController (e2e)', () => {
|
|
272
|
+
let app: INestApplication;
|
|
273
|
+
|
|
274
|
+
beforeAll(async () => {
|
|
275
|
+
const moduleFixture: TestingModule = await Test.createTestingModule({
|
|
276
|
+
imports: [AppModule],
|
|
277
|
+
}).compile();
|
|
278
|
+
|
|
279
|
+
app = moduleFixture.createNestApplication();
|
|
280
|
+
app.useGlobalPipes(new ValidationPipe({ transform: true }));
|
|
281
|
+
await app.init();
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
afterAll(async () => {
|
|
285
|
+
await app.close();
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
describe('POST /users', () => {
|
|
289
|
+
it('should create a new user (201)', () => {
|
|
290
|
+
return request(app.getHttpServer())
|
|
291
|
+
.post('/users')
|
|
292
|
+
.send({ name: 'John', email: 'john@test.com' })
|
|
293
|
+
.expect(201)
|
|
294
|
+
.expect((res) => {
|
|
295
|
+
expect(res.body).toHaveProperty('id');
|
|
296
|
+
expect(res.body.name).toBe('John');
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('should return 400 when email is invalid', () => {
|
|
301
|
+
return request(app.getHttpServer())
|
|
302
|
+
.post('/users')
|
|
303
|
+
.send({ name: 'John', email: 'invalid' })
|
|
304
|
+
.expect(400);
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
describe('GET /users/:id', () => {
|
|
309
|
+
it('should return user by id (200)', () => {
|
|
310
|
+
return request(app.getHttpServer())
|
|
311
|
+
.get('/users/1')
|
|
312
|
+
.expect(200)
|
|
313
|
+
.expect((res) => {
|
|
314
|
+
expect(res.body.id).toBe('1');
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('should return 404 when user not found', () => {
|
|
319
|
+
return request(app.getHttpServer())
|
|
320
|
+
.get('/users/999')
|
|
321
|
+
.expect(404);
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### 규칙
|
|
328
|
+
- E2E 테스트 파일은 `test/` 디렉토리에 `*.e2e-spec.ts`로 배치한다
|
|
329
|
+
- `ValidationPipe` 등 글로벌 설정을 테스트 앱에도 동일하게 적용한다
|
|
330
|
+
- `beforeAll`에서 앱을 초기화하고 `afterAll`에서 반드시 종료한다
|
|
331
|
+
- HTTP 상태 코드와 응답 본문을 모두 검증한다
|
|
332
|
+
|
|
333
|
+
---
|
|
334
|
+
|
|
335
|
+
## 8. 파일 위치 및 네이밍
|
|
336
|
+
|
|
337
|
+
| 대상 | 위치 | 예시 |
|
|
338
|
+
|------|------|------|
|
|
339
|
+
| 단위 테스트 | 소스 파일과 같은 디렉토리 (co-located) | `user.service.spec.ts` |
|
|
340
|
+
| E2E 테스트 | `test/` 디렉토리 | `test/user.e2e-spec.ts` |
|
|
341
|
+
| 테스트 유틸/팩토리 | `test/utils/` 또는 `test/factories/` | `test/factories/user.factory.ts` |
|
|
342
|
+
|
|
343
|
+
### 규칙
|
|
344
|
+
- 단위 테스트 파일 확장자: `*.spec.ts`
|
|
345
|
+
- E2E 테스트 파일 확장자: `*.e2e-spec.ts`
|
|
346
|
+
- 테스트 파일은 테스트 대상과 같은 이름을 사용한다
|
|
347
|
+
|
|
348
|
+
---
|
|
349
|
+
|
|
350
|
+
## 9. 금지 사항
|
|
351
|
+
|
|
352
|
+
- 테스트에서 실제 데이터베이스에 직접 연결 금지 (단위 테스트)
|
|
353
|
+
- `setTimeout`으로 비동기 대기 금지 (`mockResolvedValue` 등을 사용)
|
|
354
|
+
- 테스트 간 상태 공유 금지 (각 테스트는 독립적이어야 함)
|
|
355
|
+
- 구현 세부사항 테스트 금지 (private 메서드 직접 테스트 등)
|
|
356
|
+
- `any` 타입 사용 금지 (Mock 타입도 명시)
|