서비스 테스트가 실제 DB와 외부 API를 호출하고 있던 이유
문제 발생
컨트롤러 테스트가 느리고 가끔 실패했습니다. 원인을 보니 실제 리포지토리와 결제 게이트웨이가 그대로 주입되고 있었습니다.
const moduleRef = await Test.createTestingModule({
imports: [AppModule], // 앱 전체를 그대로
controllers: [OrderController],
}).compile();테스트가 외부 상태에 의존하니 CI에서만 깨지는 날이 생겼습니다.
원인 분석
테스트 모듈에 앱 전체를 넣으면 앱 전체가 뜹니다. NestJS 문서가 설명하는 Test.createTestingModule()은 애플리케이션의 모듈 구조를 그대로 반영하는 격리된 테스트 환경을 만들고, compile()로 부트스트랩합니다. 무엇을 넣을지는 우리가 정합니다 — 앱 모듈을 통째로 넣으면 실제 provider가 전부 따라옵니다.
문서는 대체 방법도 함께 제공합니다 — overrideProvider()와 useValue()로 실제 구현을 테스트 대역으로 치환할 수 있고, 이때 오버라이드할 객체의 인스턴스를 공급합니다.
그리고 테스트 종류의 구분도 명확합니다 — 단위 테스트는 개별 클래스를 격리해 검증하고, e2e 테스트는 Supertest로 실제 HTTP 요청을 컴파일된 Nest 애플리케이션에 보내 컨트롤러·서비스·프레임워크 구성요소의 상호작용 전체를 검증합니다.
해결 방안
- 필요한 것만 넣고 나머지는 대역으로 바꿉니다.
const moduleRef = await Test.createTestingModule({
controllers: [OrderController],
providers: [OrderService],
})
.overrideProvider(PAYMENT_GATEWAY)
.useValue({ charge: jest.fn().mockResolvedValue({ ok: true }) })
.compile();-
인스턴스는 컨테이너에서 꺼냅니다.
moduleRef.get()으로 정적 인스턴스를, 요청 스코프 provider는resolve()로 가져옵니다 — 둘을 헷갈리면 스코프가 다른 인스턴스를 테스트하게 됩니다. -
e2e는 별도로, 적게 둡니다.
createNestApplication()으로 실제 앱을 띄우고 Supertest로 요청합니다 — 인증 흐름처럼 조립 결과를 봐야 하는 시나리오만 여기에 남깁니다. -
DI가 필요 없는 로직은 그냥
new로 테스트합니다. 순수 함수나 도메인 규칙에 테스트 모듈을 띄우는 것은 비용만 늘립니다. -
모듈을 정리하면 테스트가 쉬워집니다. 테스트에서 오버라이드할 것이 많다는 것은 대개 그 클래스가 너무 많은 것에 의존한다는 신호입니다.
-
토큰을 export해두면 오버라이드가 쉬워집니다. 커스텀 provider 토큰을 상수로 관리하면 테스트에서도 같은 값을 그대로 씁니다.
댓글0
댓글을 남기려면 로그인이 필요해요. 로그인
아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.