@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.
@@ -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 타입도 명시)