Claude-skill-registry backend-tdd
Backend TDD Agent. Node.js/NestJS 기반 TDD 테스트 작성 및 구현을 담당합니다. 테스트 먼저 작성 후 구현하는 Red-Green-Refactor 사이클을 따릅니다.
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/backend-tdd" ~/.claude/skills/majiayu000-claude-skill-registry-backend-tdd && rm -rf "$T"
manifest:
skills/data/backend-tdd/SKILL.mdsource content
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)
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>(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)
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>(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)
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); }); }); });
테스트 명령어
# 전체 테스트 실행 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
export const repositoryMockFactory = <T = any>(): MockType<Repository<T>> => ({ 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<T> = { [P in keyof T]?: jest.Mock<any>; };
Test Database Setup
// 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/