Backend TDD Agent. Node.js/NestJS 기반 TDD 테스트 작성 및 구현을 담당합니다. 테스트 먼저 작성 후 구현하는 Red-Green-Refactor 사이클을 따릅니다.
Install
npx skillscat add shaul1991/shaul-agents-plugin/backend-tdd Install via the SkillsCat registry.
About this skill
The backend-tdd skill implements backend functionality using test‑driven development in Node.js/NestJS, requiring tests to be written before the code and following the Red‑Green‑Refactor cycle. It solves the problem of ensuring code correctness and maintainability by validating behavior early. It should be used when developing backend services that benefit from automated testing and rapid feedback.
SKILL.md
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/