home / skills / shaul1991 / shaul-agents-plugin / backend-tdd

backend-tdd skill

/skills/backend-tdd

This skill helps you implement backend TDD with red-green-refactor cycles in Node.js/NestJS, driving test-first development and reliable APIs.

npx playbooks add skill shaul1991/shaul-agents-plugin --skill backend-tdd

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
9.3 KB
---
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>(UserService);
  });

  describe('findById', () => {
    it('should return user when found', async () => {
      // Arrange
      const user = { id: 1, name: 'John', email: '[email protected]' };
      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: '[email protected]' };
      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>(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: '[email protected]' })
        .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 = <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

```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/`

Overview

This skill implements Test-Driven Development for Node.js/NestJS backend code. It follows the Red-Green-Refactor cycle to produce small, well-tested increments and integrates Jest, Supertest, and in-memory databases for fast, reliable tests. The agent writes unit, controller, and E2E tests first and then implements the minimal code needed to make them pass.

How this skill works

The agent generates failing tests that express the desired behavior, runs them to confirm failures (RED), implements the smallest amount of code to satisfy the tests (GREEN), and then refactors while keeping tests passing (REFACTOR). It uses repository mocks and test database modules for isolation, and runs E2E tests with Supertest against a Nest application instance. Coverage goals and test commands are included to enforce quality.

When to use it

  • When building or extending NestJS services and controllers using TDD.
  • When you need reliable unit tests for business logic and controller behavior.
  • When validating API flows with automated E2E tests before production deployment.
  • When you want strict coverage targets and repeatable test environments.
  • When introducing or improving CI test pipelines.

Best practices

  • Write clear, behavior-focused test names and cover happy and error paths.
  • Use repository mock factories for unit tests and an in-memory or test-container database for integration tests.
  • Keep each RED step minimal: tests should fail for the expected reason before implementing code.
  • Implement only the code required to pass tests, then refactor for readability and reuse with tests still passing.
  • Aim for the coverage targets: service >90%, controller >70%, overall >80%. Run coverage checks in CI.

Example use cases

  • Create a UserService: write unit tests for findById and create, mock repository methods, implement minimal logic to pass tests.
  • Add a new REST endpoint: write controller E2E tests with Supertest, implement route and DTOs, and iterate until tests pass.
  • Hardening error handling: add tests for NotFound and validation errors, implement and refactor consistent exception handling.
  • CI integration: run npm test and coverage in pipeline, fail builds that drop below coverage thresholds.
  • Database-dependent features: use an in-memory SQLite test module or test containers for E2E validation.

FAQ

What test runner and libraries are used?

Jest is used as the test runner; Supertest is used for E2E HTTP tests; jest.mock and jest.spyOn are used for mocking.

How do I run only E2E tests?

Use the provided script, typically npm run test:e2e, which boots a Nest application and runs Supertest suites.