dev.to

NestJS Testing Module: Provider Overrides (with Cheat Sheet)

NestJS 테스트 모듈의 Provider 교체와 사용법 요약

NestJS의 테스트 모듈은 단순한 mock 저장소가 아니라 실제 애플리케이션과 같은 의존성 주입(DI) 컨테이너를 구성합니다. 글은 provider 교체 방법과 get()·resolve()의 차이, 요청 범위 provider의 컨텍스트 공유, 가드와 파이프를 바꾸는 E2E 테스트 방법을 설명합니다.

AI 요약

NestJS 테스트에서 provider를 교체할 때는 객체를 직접 조립하거나 Jest의 모듈 mock으로 우회하기보다, Nest의 테스트 모듈이 제공하는 DI 흐름을 활용하는 편이 좋습니다. Test.createTestingModule()은 애플리케이션과 같은 모듈 메타데이터를 바탕으로 실제 DI 컨테이너를 만들고, overrideProvider()는 컴파일 전에 provider 등록을 바꿉니다. 따라서 테스트 대상은 실제 앱과 같은 경로로 의존성을 주입받습니다.

Provider 교체 방법

overrideProvider(Token).useValue(mock)는 미리 만든 객체나 Jest mock을 주입합니다. mock 자체는 Nest의 DI나 생명주기에 참여하지 않으므로 빠르고 명확하며, 호출 기록도 바로 확인할 수 있습니다. 동작이 필요한 가짜 구현에는 useClass(FakeImpl)을 쓸 수 있습니다. 이 경우 가짜 클래스의 생성자도 Nest가 처리하므로 다른 테스트 의존성을 주입받습니다.

가짜 객체를 컨테이너의 설정값이나 다른 mock으로 구성해야 한다면 useFactory({ factory, inject })를 사용합니다. inject에 적은 의존성은 같은 테스트 컨테이너에서 해석되므로, 다른 provider 교체 결과도 활용합니다. 여러 provider를 일일이 대체하기 번거로운 모듈에서는 useMocker()가 나머지 provider를 자동 mock하도록 할 수 있습니다. 다만 자동 mock 메서드는 기본적으로 undefined를 반환합니다. 테스트가 실제로 확인해야 하는 provider까지 자동 mock하면 기능을 검증하지 않고도 테스트가 통과할 수 있으므로, 검증 대상은 명시적인 mock으로 둬야 합니다.

`get()`과 `resolve()`의 차이

컴파일된 모듈에서 moduleRef.get(Token)은 정적 등록을 조회하며, 기본 범위인 DEFAULT provider에 사용합니다. REQUESTTRANSIENT provider는 단일한 정적 인스턴스가 없으므로 get()을 호출하면 오류가 납니다. 이때는 비동기 메서드인 await moduleRef.resolve(Token)을 사용합니다.

resolve()를 호출할 때마다 기본적으로 새 인스턴스를 받습니다. 한 요청 안에서 같은 provider 인스턴스를 여러 번 사용하려면 ContextIdFactory.create()로 컨텍스트 ID를 한 번 만들고, 공유할 모든 resolve() 호출에 같은 ID를 전달해야 합니다. 반대로 서로 다른 컨텍스트 ID를 사용하면 요청 범위 인스턴스가 분리됩니다. get(Token, { strict: false })는 검색 범위를 다른 모듈까지 넓힐 뿐, 요청 범위 provider를 가져오게 해주지는 않습니다.

파이프라인 구성 요소와 주의점

Provider 외에도 overrideGuard(), overridePipe(), overrideInterceptor(), overrideFilter()로 요청 파이프라인 요소를 대체할 수 있습니다. 실제 HTTP 요청 흐름을 검증하려면 createNestApplication()으로 앱을 만들고, Supertest 등으로 요청을 보내야 합니다. 서비스 메서드 하나를 시험하는 단위 테스트에 매번 전체 애플리케이션을 띄울 필요는 없습니다.

모든 override는 .compile() 전에 선언해야 합니다. 컴파일이 끝난 테스트 모듈은 이미 구성된 컨테이너이므로 나중에 교체할 수 없습니다. 테스트에서도 모듈의 export 경계와 생명주기 훅은 실제 앱처럼 적용됩니다. onModuleInit() 같은 훅이 실제 연결을 열면 테스트에서도 실행될 수 있으므로 관련 provider를 교체해야 합니다. 전체 모듈을 바꾸는 overrideModule()은 범위가 크므로, 우선 필요한 token만 교체하는 편이 낫습니다.