Skip to content

jest로 도전한 NestJS 인터셉터 통합테스트

mintaek edited this page Dec 3, 2024 · 2 revisions

통합테스트를 하는 이유가 무엇인가요?

인터셉터에서 각 유저에 대하여 DB와의 Connection 및 트랜잭션을 관리하는 로직이 존재합니다. 이는 DB에 대한 의존성이 강하게 결합되어있어 단위테스트 만으로는 한계가 있다고 판단하여 통합테스트로 진행하였습니다.

통합테스트 방법

TestContainer를 사용하여 테스트를 진행하였습니다.

테스트 컨테이너 공식문서

TestContainer는 코드를 통해 간편하게 도커 컨테이너를 관리할 수 있습니다.

const TEST_SESSION_ID = 'db12345678';
const TEST_REQUEST = {
  sessionID: TEST_SESSION_ID,
  dbConnection: null,
};

beforeAll(async () => {
  dbContainer = await new MySqlContainer()
    .withUsername(TEST_SESSION_ID.substring(0, 10))
    .withUserPassword(TEST_SESSION_ID)
    .withDatabase(TEST_SESSION_ID)
    .withExposedPorts(3306)
    .withCommand(['--max_connections=1'])
    .start();
});

afterAll(async () => {
  await dbContainer.stop();
});

Mocking

테스트코드를 작성할때 mocking은 매우 중요합니다.

mocking의 대상을 지정하고 시나리오에 따라 적절한 mocking을 수행하는 것이 테스트의 의의에 큰 영향 끼칩니다.

Mocking 방법

우선 mocking은 jest-mock-extended 를 사용하였습니다.

가장 기본적인 모킹 방법은 jest.fn()을 사용하는 것 입니다. 하지만 이는 단점이 존재하는데, 타입 기반 언어가 아닌 js 특성상 클래스,인터페이스 자체를 모킹하지 못하고 각 함수마다 모킹을 진행해야합니다.

TypeScirpt를 이용하고있는 QLab에서 이는 불필요한 작업을 초래할 수 있습니다.

Mocking 대상

통합테스트라고 모든 것을 mocking 없이 할 수는 없었습니다.

대표적인 예시를 들면, Interceptor는 nest로 부터 ExecutionContextCallhandler 를 인자값으로 받는데 이는 Nest 실행환경에서 요청시에 대입해주는 값 이므로 이는 mocking없이 불가능합니다.

@Injectable()
export class UserDBConnectionInterceptor implements NestInterceptor {
  constructor() {}

	//인자값을 Nest로 부터 주입받음
  async intercept(
    context: ExecutionContext,
    next: CallHandler,
  ): Promise<Observable<any>> {
	   //요청 로직
    return next.handle().pipe(
     //응답 로직
    );
  }
}

mocking대상은 총 4가지입니다.

  • ExecutionContext,Callhandler
    • nest에서 주입해주는 intercept 메서드의 인자값
  • ConfigService
    • DB connection에 메타데이터를 가지고 있는 객체
  • HttpArgumentsHost
    • ExecutionContext내에 request를 가져오기 위한 객체

코드레벨에서 구현은 다음과 같이 진행하였습니다.

//테스트 데이터
const TEST_SESSION_ID = 'db12345678';
const TEST_REQUEST = {
  sessionID: TEST_SESSION_ID,
  dbConnection: null,
};

//context 및 httpArgument mocking
const mockContext = mock<ExecutionContext>();
const mockHttpArgumentsHost = mock<HttpArgumentsHost>();
mockHttpArgumentsHost.getRequest.mockReturnValue(TEST_REQUEST);
mockContext.switchToHttp.mockReturnValue(mockHttpArgumentsHost);
 
//configService mocking
const mockConfigService = mock<ConfigService>();
mockConfigService.get.mockImplementation((key: string) => {
  const config = {
    QUERY_DB_HOST: dbContainer.getHost(),
    QUERY_DB_PORT: dbContainer.getMappedPort(3306),
  };
  return config[key];
});

//callHandler mocking
const mockCallHandler = mock<CallHandler>();

//정상 응답에 대해서
mockCallHandler.handle.mockReturnValue(of('test response'));
//특정 에러에 대해서
mockCallHandler.handle.mockImplementation(() =>
	throwError(() => new DataLimitExceedException()),
);
//일반적인 에러에 대해서
mockCallHandler.handle.mockImplementation(() =>
  throwError(() => new Error()),
);

테스트 목적

테스트 목적을 세우려면 해당 객체가 어떤 책임이 있는지를 인지해야합니다.

인터셉터는 요청과 응답을 둘다 관리하는 객체입니다.

이에 따른 책임을 분리하였습니다.

요청

  • User와의 DB Connection 생성을 관리.

응답

  • 응답 상태에 따른 트랜잭션 commit,rollback 관리.
  • Connection 종료.

위 3가지 조건이 잘 지켜지는지에 대한 시나리오를 작성하였습니다.

실제 작성한 테스트 시나리오

테스트 코드
Clone this wiki locally