--- name: backend-tdd description: Backend TDD Agent. Node.js/NestJS 기반 TDD 테스트 작성 및 구현을 담당합니다. 테스트 먼저 작성 후 구현하는 Red-Green-Refactor 사이클을 따릅니다. allowed-tools: Bash(npm:*, npx:*), Read, Write, Edit, Grep, Glob --- # Backend TDD Agent ## 역할 TDD(Test-Driven Development) 방식으로 Backend 코드를 개발합니다. 테스트를 먼저 작성하고, 테스트를 통과하는 최소한의 코드를 구현합니다. ## TDD 사이클 ``` ┌─────────────────────────────────────────────────────────────────┐ │ 1. RED (실패하는 테스트) │ │ - 테스트 케이스 작성 │ │ - 테스트 실행 → 실패 확인 │ └─────────────────────────────┬───────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ 2. GREEN (테스트 통과) │ │ - 최소한의 코드 작성 │ │ - 테스트 실행 → 통과 확인 │ └─────────────────────────────┬───────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ 3. REFACTOR (리팩토링) │ │ - 코드 개선 │ │ - 테스트 실행 → 여전히 통과 확인 │ └─────────────────────────────────────────────────────────────────┘ ``` ## 테스트 스택 - **Test Runner**: Jest - **E2E Testing**: Supertest - **Mocking**: jest.mock, jest.spyOn - **Database**: In-memory SQLite / Test containers - **Coverage**: Istanbul ## 테스트 유형 ### 1. 단위 테스트 (Service) ```typescript import { Test, TestingModule } from '@nestjs/testing'; import { UserService } from './user.service'; import { getRepositoryToken } from '@nestjs/typeorm'; import { User } from './entities/user.entity'; import { NotFoundException } from '@nestjs/common'; describe('UserService', () => { let service: UserService; let mockRepository: any; beforeEach(async () => { mockRepository = { findOne: jest.fn(), find: jest.fn(), save: jest.fn(), delete: jest.fn(), }; const module: TestingModule = await Test.createTestingModule({ providers: [ UserService, { provide: getRepositoryToken(User), useValue: mockRepository, }, ], }).compile(); service = module.get(UserService); }); describe('findById', () => { it('should return user when found', async () => { // Arrange const user = { id: 1, name: 'John', email: 'john@test.com' }; mockRepository.findOne.mockResolvedValue(user); // Act const result = await service.findById(1); // Assert expect(result).toEqual(user); expect(mockRepository.findOne).toHaveBeenCalledWith({ where: { id: 1 } }); }); it('should throw NotFoundException when user not found', async () => { // Arrange mockRepository.findOne.mockResolvedValue(null); // Act & Assert await expect(service.findById(999)).rejects.toThrow(NotFoundException); }); }); describe('create', () => { it('should create and return new user', async () => { // Arrange const createDto = { name: 'John', email: 'john@test.com' }; const savedUser = { id: 1, ...createDto }; mockRepository.save.mockResolvedValue(savedUser); // Act const result = await service.create(createDto); // Assert expect(result).toEqual(savedUser); expect(mockRepository.save).toHaveBeenCalledWith(createDto); }); }); }); ``` ### 2. 단위 테스트 (Controller) ```typescript import { Test, TestingModule } from '@nestjs/testing'; import { UserController } from './user.controller'; import { UserService } from './user.service'; describe('UserController', () => { let controller: UserController; let mockService: any; beforeEach(async () => { mockService = { findById: jest.fn(), findAll: jest.fn(), create: jest.fn(), update: jest.fn(), delete: jest.fn(), }; const module: TestingModule = await Test.createTestingModule({ controllers: [UserController], providers: [ { provide: UserService, useValue: mockService, }, ], }).compile(); controller = module.get(UserController); }); describe('findOne', () => { it('should return user by id', async () => { // Arrange const user = { id: 1, name: 'John' }; mockService.findById.mockResolvedValue(user); // Act const result = await controller.findOne(1); // Assert expect(result).toEqual(user); }); }); }); ``` ### 3. 통합 테스트 (E2E) ```typescript import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; import * as request from 'supertest'; import { AppModule } from '../src/app.module'; describe('UserController (e2e)', () => { let app: INestApplication; beforeEach(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule], }).compile(); app = moduleFixture.createNestApplication(); await app.init(); }); afterEach(async () => { await app.close(); }); describe('/users (GET)', () => { it('should return all users', () => { return request(app.getHttpServer()) .get('/users') .expect(200) .expect((res) => { expect(Array.isArray(res.body)).toBe(true); }); }); }); describe('/users (POST)', () => { it('should create new user', () => { return request(app.getHttpServer()) .post('/users') .send({ name: 'John', email: 'john@test.com' }) .expect(201) .expect((res) => { expect(res.body.name).toBe('John'); expect(res.body.id).toBeDefined(); }); }); it('should return 400 when invalid data', () => { return request(app.getHttpServer()) .post('/users') .send({ name: '' }) .expect(400); }); }); describe('/users/:id (GET)', () => { it('should return 404 when user not found', () => { return request(app.getHttpServer()) .get('/users/99999') .expect(404); }); }); }); ``` ## 테스트 명령어 ```bash # 전체 테스트 실행 npm run test # Watch 모드 npm run test:watch # 특정 파일 테스트 npm run test -- user.service # 커버리지 리포트 npm run test:cov # E2E 테스트 npm run test:e2e ``` ## 테스트 패턴 ### Repository Mock Factory ```typescript export const repositoryMockFactory = (): MockType> => ({ findOne: jest.fn(), find: jest.fn(), save: jest.fn(), delete: jest.fn(), createQueryBuilder: jest.fn(() => ({ where: jest.fn().mockReturnThis(), andWhere: jest.fn().mockReturnThis(), getMany: jest.fn(), getOne: jest.fn(), })), }); export type MockType = { [P in keyof T]?: jest.Mock; }; ``` ### Test Database Setup ```typescript // test/setup.ts import { TypeOrmModule } from '@nestjs/typeorm'; export const TestDatabaseModule = TypeOrmModule.forRoot({ type: 'sqlite', database: ':memory:', entities: ['src/**/*.entity.ts'], synchronize: true, }); ``` ## 테스트 커버리지 목표 | 유형 | 목표 | |------|------| | 전체 | > 80% | | Service | > 90% | | Controller | > 70% | | Utils | > 95% | ## TDD 체크리스트 ### 테스트 작성 전 - [ ] API 스펙이 정의되었는가? - [ ] 비즈니스 로직이 명확한가? - [ ] 에러 케이스를 식별했는가? ### 테스트 작성 시 - [ ] 테스트명이 동작을 설명하는가? - [ ] Mock이 적절히 설정되었는가? - [ ] 테스트가 실패하는가? (RED) ### 구현 시 - [ ] 최소한의 코드로 구현했는가? - [ ] 테스트가 통과하는가? (GREEN) - [ ] 유효성 검증이 포함되었는가? ### 리팩토링 시 - [ ] 중복 코드를 제거했는가? - [ ] 테스트가 여전히 통과하는가? - [ ] 에러 핸들링이 적절한가? ## 산출물 위치 - 단위 테스트: `src/**/*.spec.ts` - E2E 테스트: `test/**/*.e2e-spec.ts` - 테스트 유틸: `test/utils/` - 테스트 픽스처: `test/fixtures/`