NestJS에서 유닛 테스트를 작성할 때, 많은 의존성을 모킹(Mock)해야 하는 상황이 자주 발생한다.
Repository, Service, ConfigService, Cache 등 다양한 Provider를 직접 등록하고
jest.fn()을 이용해 일일이 모킹하면 코드가 복잡해지고 관리가 어려워진다.
이 문제를 해결하기 위해 AutoMock을 활용할 수 있다.
이번 포스팅에서는 @automock/jest를 사용해 번거로운 Provider 등록 과정을 자동화하고,
NestJS의 DI 흐름을 그대로 모사하여 테스트하는 방법을 살펴보자.
기존의 Mocking 방식과 문제점
NestJS에서 유닛 테스트를 작성할 때, 종속된 서비스나 레포지토리를
직접 `jest.fn()`을 사용하여 일일이 Mocking해야 했다.
이를 위해 `@nestjs/testing`의 `Test.createTestingModule`을 사용하여
`providers` 배열에 `useValue` 또는 `useFactory`를 통해 Mock 객체를 수동으로 주입해야 했다.
일일이 주입해야 하므로, Provider가 많을수록 복잡해지는 문제가 있었다.
const mockUserRepository = {
findOne: jest.fn(),
};
const mockConfigService = {
get: jest.fn(),
};
const mockJwtService = {
signAsync: jest.fn(),
verifyAsync: jest.fn(),
};
const mockUserService = {
create: jest.fn(),
};
const mockCacheManager = {
set: jest.fn(),
};
describe('AuthService', () => {
let authService: AuthService;
let userRepository: Repository<User>;
let configService: ConfigService;
let jwtService: JwtService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
AuthService,
{
provide: getRepositoryToken(User),
useValue: mockUserRepository,
},
{
provide: ConfigService,
useValue: mockConfigService,
},
{
provide: JwtService,
useValue: mockJwtService,
},
// ...
],
}).compile();
authService = module.get<AuthService>(AuthService);
userRepository = module.get<Repository<User>>(getRepositoryToken(User));
configService = module.get<ConfigService>(ConfigService);
jwtService = module.get<JwtService>(JwtService);
});
// ... 테스트 코드 ...
});
기존 방식의 단점
• providers: [...] 배열에 Mock 객체를 매번 수동으로 등록해야 한다.
• 의존하는 객체가 많아질수록 테스트 코드가 복잡해진다.
• Mock 객체를 직접 관리해야 해서 유지보수에 부담이 크다.
• 새로운 Provider가 추가될 때마다 일일이 변경해야 하는 번거롭다.
이러한 문제를 해결하기 위해 @automock/jest를 적용하면 된다.
AutoMock(@automock/jest) 적용
@automock/jest는 이러한 번거로운 과정을 자동화해주는 라이브러리다.
NestJS에서 필요한 Provider를 알아서 찾아 Mock 버전으로 주입해주므로,
기존 대비 훨씬 코드가 간결해진다.
패키지 설치
npm i -D @automock/jest @automock/adapters.nestjs
@automock/jest 적용 방식
import { TestBed } from '@automock/jest';
describe('MovieService', () => {
let movieService: MovieService;
let movieRepository: jest.Mocked<Repository<Movie>>;
beforeEach(async () => {
// TestBed.create(MovieService) 호출 시, 의존성이 자동으로 Mock 처리된다.
const { unit, unitRef } = TestBed.create(MovieService).compile();
/**
* unit: 실제 테스트 대상인 MovieService의 인스턴스 (Mock 아님)
* unitRef: Nest DI 컨테이너처럼 작동하는 레퍼런스 (Mock을 get(token)으로 조회)
*/
movieService = unit;
movieRepository = unitRef.get(getRepositoryToken(Movie) as string);
});
// ... 테스트 코드 ...
});
- 기존 방식: beforeEach에서 Mock 객체를 만들고, providers에 수동 등록
- AutoMock 방식: TestBed.create(MyService).compile()만 하면, NestJS에서 주입받는 의존성을 자동으로 Mock 처리
동작 원리를 살펴보자.
1. TestBed.create(MyService).compile()를 호출하면,
MyService가 의존하는 모든 Provider를 NestJS 컨테이너처럼 자동 스캔하여 Mock 버전을 생성한다.
2. unit은 실제 서비스의 인스턴스(테스트 대상)이고,
unitRef는 Nest 컨테이너처럼 get(token)을 통해 Mock 객체를 반환한다.
3. 리포지토리나 ConfigService 같은 객체는 jest.Mocked<Type> 형태로 제공되어,
mockResolvedValue(), mockImplementation() 등의 Jest 함수를 바로 사용할 수 있어 편리하다.
항목 | 기존 방식 | @automock/jest 적용 후 |
의존성 주입 | providers: [...]에 직접 Mock 객체 등록 | TestBed.create() 호출만으로 자동 처리 |
의존성 접근 | module.get(토큰) | unitRef.get(토큰) |
유지보수 | 의존성이 많아질수록 복잡해짐 | 간결하고 확장 가능 |
Mock 생성 | jest.fn()을 이용해 직접 생성 | 자동 생성된 Mock 사용 |
적용 후 장점을 살펴보면
- 코드 간소화: 테스트를 위한 모킹 로직이 줄어듦으로써, 테스트 파일이 한결 가독성 있어진다.
- NestJS 친화적인 DI: NestJS의 Provider/DI 매커니즘을 그대로 사용하므로, 실제 애플리케이션 구조와 유사한 테스트가 가능하다.
- 유지보수성 향상: Provider가 늘어나도 자동으로 Mock가 생성되므로, 설정해야 하는 부분이 최소화된다.
- 커버리지 상승: 모킹 작업 부담이 줄어드니, 로직 테스트에 집중할 수 있어 자연스럽게 커버리지를 높일 수 있다.
Automock 적용 후 간결해진 코드 예시
import { TestBed } from '@automock/jest';
describe('MovieService', () => {
let movieService: MovieService;
let movieRepository: jest.Mocked<Repository<Movie>>;
let directorRepository: jest.Mocked<Repository<Director>>;
let genreRepository: jest.Mocked<Repository<Genre>>;
let userRepository: jest.Mocked<Repository<User>>;
let dataSource: jest.Mocked<DataSource>;
let commonService: jest.Mocked<CommonService>;
let cacheManager: jest.Mocked<Cache>;
beforeEach(async () => {
// 오토목 TestBed 사용
const { unit, unitRef } = TestBed.create(MovieService).compile();
movieService = unit;
movieRepository = unitRef.get(getRepositoryToken(Movie) as string);
directorRepository = unitRef.get(getRepositoryToken(Director) as string);
genreRepository = unitRef.get(getRepositoryToken(Genre) as string);
userRepository = unitRef.get(getRepositoryToken(User) as string);
dataSource = unitRef.get(DataSource);
commonService = unitRef.get(CommonService);
cacheManager = unitRef.get(CACHE_MANAGER);
});
// ... 테스트 코드 ...
});
providers: [...] 배열에 useValue로 Mock 객체를 일일이 등록하거나,
jest.fn()을 통해 중복 설정하지 않아도 되므로 테스트 코드가 상당히 간결해진다.
JEST 커버리지 100% + E2E 테스트
@automock/jest를 적용하여 기존보다 훨씬 빠르고 쉽게 테스트를 작성할 수 있었고,
최종적으로 Jest 커버리지 100%를 달성하였으며,
유닛(Unit), 통합(Integration), E2E(End-to-End) 테스트까지 완료했다.
AutoMock을 활용하면 NestJS 테스트에서 필요한 의존성을 자동으로 Mocking하여,
테스트 코드의 복잡도를 줄이고 유지보수성을 극대화할 수 있다.
복잡한 의존성으로 인해 테스트 작성을 미뤘다면,
AutoMock을 도입하여 안정적이고 효율적인 테스트 환경을 구축해보길 바란다.
Happy Testing !!! 🧪👻
참조 :
https://docs.nestjs.com/fundamentals/testing
https://docs.nestjs.com/recipes/suites#suites-formerly-automock
https://jestjs.io/docs/es6-class-mocks#automatic-mock
'Web > NestJS' 카테고리의 다른 글
[Nest.JS] 구글, 네이버, 카카오 소셜로그인 구현 - 2 (1) | 2023.11.04 |
---|---|
[Nest.JS] 구글, 네이버, 카카오 소셜로그인 구현 - 1 (1) | 2023.10.16 |
[Nest.JS] Nest.JS에서 cache-manager를 활용한 캐싱 방법 (0) | 2023.09.17 |
[Nest.JS] Slack에 다양한 통계데이터 알림 자동화 (0) | 2023.07.02 |
[Nest.JS] TypeOrm에서 페이지네이션 구현 (last_id, size) (0) | 2023.05.05 |