icon

단위 테스트 작성과 모킹


 단위 테스트는 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. 실패하는 테스트 작성
  2. 테스트를 통과하는 최소한의 코드 작성
  3. 리팩토링
// 1. 실패하는 테스트
it('should create a user', async () => {
  const dto = { name: 'John', email: '[email protected]' };
  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

  1. 각 테스트는 독립적이어야 하며, 다른 테스트에 영향을 주지 않아야 합니다.
  2. 테스트 설명은 명확하고 구체적이어야 합니다.
  3. 테스트 커버리지를 높이되, 의미 있는 테스트에 집중하세요.
  4. 테스트 데이터와 예상 결과를 명확히 정의하세요.
  5. 모킹은 필요한 경우에만 사용하고, 과도한 사용은 피하세요.
  6. 비동기 코드 테스트 시 적절한 비동기 처리 방법을 사용하세요.
  7. 에러 케이스와 경계 조건도 반드시 테스트하세요.
  8. 테스트 코드도 실제 코드만큼 깨끗하고 유지보수 가능하게 작성하세요.
  9. CI/CD 파이프라인에 단위 테스트를 포함시켜 자동화하세요.
  10. 정기적으로 테스트 스위트를 리뷰하고 개선하세요.

 단위 테스트는 애플리케이션의 안정성과 신뢰성을 보장하는 중요한 과정입니다.

 Jest와 NestJS의 테스팅 도구를 활용하여 효과적인 테스트를 작성하고, 지속적인 개선을 통해 높은 품질의 코드를 유지할 수 있습니다.