본문으로 건너뛰기
개발 머꼬
개발 노트NestJS
hohyeon.dev21

서비스 테스트가 실제 DB와 외부 API를 호출하고 있던 이유

  • #Engineering Note
  • #NestJS
  • #Testing

문제 발생

컨트롤러 테스트가 느리고 가끔 실패했습니다. 원인을 보니 실제 리포지토리와 결제 게이트웨이가 그대로 주입되고 있었습니다.

const moduleRef = await Test.createTestingModule({
  imports: [AppModule],         // 앱 전체를 그대로
  controllers: [OrderController],
}).compile();

테스트가 외부 상태에 의존하니 CI에서만 깨지는 날이 생겼습니다.

원인 분석

테스트 모듈에 앱 전체를 넣으면 앱 전체가 뜹니다. NestJS 문서가 설명하는 Test.createTestingModule()애플리케이션의 모듈 구조를 그대로 반영하는 격리된 테스트 환경을 만들고, compile()로 부트스트랩합니다. 무엇을 넣을지는 우리가 정합니다 — 앱 모듈을 통째로 넣으면 실제 provider가 전부 따라옵니다.

문서는 대체 방법도 함께 제공합니다 — overrideProvider()useValue()로 실제 구현을 테스트 대역으로 치환할 수 있고, 이때 오버라이드할 객체의 인스턴스를 공급합니다.

그리고 테스트 종류의 구분도 명확합니다 — 단위 테스트는 개별 클래스를 격리해 검증하고, e2e 테스트는 Supertest로 실제 HTTP 요청을 컴파일된 Nest 애플리케이션에 보내 컨트롤러·서비스·프레임워크 구성요소의 상호작용 전체를 검증합니다.

해결 방안

  1. 필요한 것만 넣고 나머지는 대역으로 바꿉니다.
const moduleRef = await Test.createTestingModule({
  controllers: [OrderController],
  providers: [OrderService],
})
  .overrideProvider(PAYMENT_GATEWAY)
  .useValue({ charge: jest.fn().mockResolvedValue({ ok: true }) })
  .compile();
  1. 인스턴스는 컨테이너에서 꺼냅니다. moduleRef.get()으로 정적 인스턴스를, 요청 스코프 provider는 resolve()로 가져옵니다 — 둘을 헷갈리면 스코프가 다른 인스턴스를 테스트하게 됩니다.

  2. e2e는 별도로, 적게 둡니다. createNestApplication()으로 실제 앱을 띄우고 Supertest로 요청합니다 — 인증 흐름처럼 조립 결과를 봐야 하는 시나리오만 여기에 남깁니다.

  3. DI가 필요 없는 로직은 그냥 new로 테스트합니다. 순수 함수나 도메인 규칙에 테스트 모듈을 띄우는 것은 비용만 늘립니다.

  4. 모듈을 정리하면 테스트가 쉬워집니다. 테스트에서 오버라이드할 것이 많다는 것은 대개 그 클래스가 너무 많은 것에 의존한다는 신호입니다.

  5. 토큰을 export해두면 오버라이드가 쉬워집니다. 커스텀 provider 토큰을 상수로 관리하면 테스트에서도 같은 값을 그대로 씁니다.

공식 문서

마지막 수정

좋아요북마크

댓글0

아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.