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.md
source 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/