단위 테스트 작성과 모킹
단위 테스트는 NestJS 애플리케이션의 품질과 안정성을 보장하는 핵심 요소입니다.
개별 컴포넌트의 기능을 검증하고, 리팩토링과 새로운 기능 추가 시 발생할 수 있는 회귀 오류를 방지합니다.
NestJS는 Jest를 기본 테스트 프레임워크로 사용하며, 의존성 주입 시스템과 잘 통합되어 효과적인 단위 테스트 작성을 지원합니다.
테스트 환경 설정
NestJS 프로젝트는 기본적으로 Jest 설정이 포함되어 있습니다.
추가 설정이 필요한 경우
// jest.config.js
module.exports = {
moduleFileExtensions: ['js', 'json', 'ts'],
rootDir: 'src',
testRegex: '.*\\.spec\\.ts$',
transform: {
'^.+\\.(t|j)s$': 'ts-jest',
},
collectCoverageFrom: ['**/*.(t|j)s'],
coverageDirectory: '../coverage',
testEnvironment: 'node',
};
컨트롤러 테스트
컨트롤러 테스트 예시
import { Test, TestingModule } from '@nestjs/testing';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
describe('UsersController', () => {
let controller: UsersController;
let service: UsersService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [UsersController],
providers: [{
provide: UsersService,
useValue: {
findAll: jest.fn().mockResolvedValue([{ id: 1, name: 'John Doe' }]),
},
}],
}).compile();
controller = module.get<UsersController>(UsersController);
service = module.get<UsersService>(UsersService);
});
it('should return an array of users', async () => {
const result = await controller.findAll();
expect(result).toEqual([{ id: 1, name: 'John Doe' }]);
expect(service.findAll).toHaveBeenCalled();
});
});
서비스 테스트
서비스 테스트 예시
import { Test, TestingModule } from '@nestjs/testing';
import { UsersService } from './users.service';
import { getRepositoryToken } from '@nestjs/typeorm';
import { User } from './user.entity';
describe('UsersService', () => {
let service: UsersService;
let mockRepository;
beforeEach(async () => {
mockRepository = {
find: jest.fn(),
findOne: jest.fn(),
save: jest.fn(),
};
const module: TestingModule = await Test.createTestingModule({
providers: [
UsersService,
{
provide: getRepositoryToken(User),
useValue: mockRepository,
},
],
}).compile();
service = module.get<UsersService>(UsersService);
});
it('should find all users', async () => {
const users = [{ id: 1, name: 'John' }, { id: 2, name: 'Jane' }];
mockRepository.find.mockResolvedValue(users);
const result = await service.findAll();
expect(result).toEqual(users);
expect(mockRepository.find).toHaveBeenCalled();
});
});
파이프와 가드 테스트
파이프 테스트 예시
import { ValidationPipe } from './validation.pipe';
import { ArgumentMetadata } from '@nestjs/common';
describe('ValidationPipe', () => {
let pipe: ValidationPipe;
beforeEach(() => {
pipe = new ValidationPipe();
});
it('should be defined', () => {
expect(pipe).toBeDefined();
});
it('should validate and transform', async () => {
const metadata: ArgumentMetadata = {
type: 'body',
metatype: class TestDto {
name: string;
age: number;
},
};
const value = { name: 'John', age: '30' };
const result = await pipe.transform(value, metadata);
expect(result).toEqual({ name: 'John', age: 30 });
});
});
모킹과 Test Doubles
Jest의 모킹 기능을 사용하여 외부 의존성을 효과적으로 처리할 수 있습니다.
jest.mock('./external.service');
import { ExternalService } from './external.service';
describe('UserService', () => {
let userService: UserService;
let externalService: jest.Mocked<ExternalService>;
beforeEach(() => {
externalService = new ExternalService() as jest.Mocked<ExternalService>;
userService = new UserService(externalService);
});
it('should get user data', async () => {
externalService.getData.mockResolvedValue({ id: 1, name: 'John' });
const result = await userService.getUserData(1);
expect(result).toEqual({ id: 1, name: 'John' });
expect(externalService.getData).toHaveBeenCalledWith(1);
});
});
Stub, Spy, Mock의 활용
- Stub : 특정 메소드 호출에 대해 미리 정의된 응답을 반환
- Spy : 메소드 호출을 추적하고 원래 구현을 유지
- Mock : 전체 객체나 모듈을 대체하고 행동을 검증
// Stub 예시
const stubRepository = {
find: () => Promise.resolve([{ id: 1, name: 'John' }]),
};
// Spy 예시
const spy = jest.spyOn(service, 'findOne');
await service.findOne(1);
expect(spy).toHaveBeenCalledWith(1);
// Mock 예시
jest.mock('./user.repository');
const mockRepository = require('./user.repository');
mockRepository.find.mockResolvedValue([{ id: 1, name: 'John' }]);
비동기 코드 테스트
Promise와 Observable 테스트
it('should handle promise', async () => {
const result = await service.asyncMethod();
expect(result).toBe('value');
});
it('should handle observable', (done) => {
service.observableMethod().subscribe(
(result) => {
expect(result).toBe('value');
done();
},
(error) => done(error)
);
});
테스트 주도 개발 (TDD)
TDD 접근 방식을 NestJS 프로젝트에 적용
- 실패하는 테스트 작성
- 테스트를 통과하는 최소한의 코드 작성
- 리팩토링
// 1. 실패하는 테스트
it('should create a user', async () => {
const dto = { name: 'John', email: 'john@example.com' };
const result = await controller.create(dto);
expect(result).toHaveProperty('id');
expect(result.name).toBe(dto.name);
});
// 2. 테스트 통과를 위한 코드
@Post()
async create(@Body() createUserDto: CreateUserDto) {
return this.usersService.create(createUserDto);
}
// 3. 리팩토링 및 추가 테스트
복잡한 비즈니스 로직 테스트
복잡한 조건부 로직에 대한 테스트
describe('PricingService', () => {
it('should calculate correct price for premium users', () => {
const service = new PricingService();
const price = service.calculatePrice({ isPremium: true, itemCount: 5 });
expect(price).toBe(45); // 10% 할인 적용
});
it('should calculate correct price for bulk purchase', () => {
const service = new PricingService();
const price = service.calculatePrice({ isPremium: false, itemCount: 20 });
expect(price).toBe(180); // 10% 대량 구매 할인 적용
});
});
테스트 코드 구조화
테스트 코드의 가독성과 유지보수성을 높이기 위한 패턴
describe('UserService', () => {
describe('createUser', () => {
it('should create a new user', async () => {
// 테스트 코드
});
it('should throw an error if email is already taken', async () => {
// 테스트 코드
});
});
describe('updateUser', () => {
// 업데이트 관련 테스트
});
});
Best Practices
- 각 테스트는 독립적이어야 하며, 다른 테스트에 영향을 주지 않아야 합니다.
- 테스트 설명은 명확하고 구체적이어야 합니다.
- 테스트 커버리지를 높이되, 의미 있는 테스트에 집중하세요.
- 테스트 데이터와 예상 결과를 명확히 정의하세요.
- 모킹은 필요한 경우에만 사용하고, 과도한 사용은 피하세요.
- 비동기 코드 테스트 시 적절한 비동기 처리 방법을 사용하세요.
- 에러 케이스와 경계 조건도 반드시 테스트하세요.
- 테스트 코드도 실제 코드만큼 깨끗하고 유지보수 가능하게 작성하세요.
- CI/CD 파이프라인에 단위 테스트를 포함시켜 자동화하세요.
- 정기적으로 테스트 스위트를 리뷰하고 개선하세요.
단위 테스트는 애플리케이션의 안정성과 신뢰성을 보장하는 중요한 과정입니다.
Jest와 NestJS의 테스팅 도구를 활용하여 효과적인 테스트를 작성하고, 지속적인 개선을 통해 높은 품질의 코드를 유지할 수 있습니다.