-
Notifications
You must be signed in to change notification settings - Fork 1
jest로 도전한 NestJS 인터셉터 통합테스트
인터셉터에서 각 유저에 대하여 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은 jest-mock-extended
를 사용하였습니다.
가장 기본적인 모킹 방법은 jest.fn()을 사용하는 것 입니다. 하지만 이는 단점이 존재하는데, 타입 기반 언어가 아닌 js 특성상 클래스,인터페이스 자체를 모킹하지 못하고 각 함수마다 모킹을 진행해야합니다.
TypeScirpt를 이용하고있는 QLab에서 이는 불필요한 작업을 초래할 수 있습니다.
통합테스트라고 모든 것을 mocking 없이 할 수는 없었습니다.
대표적인 예시를 들면, Interceptor는 nest로 부터 ExecutionContext
와 Callhandler
를 인자값으로 받는데 이는 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가지 조건이 잘 지켜지는지에 대한 시나리오를 작성하였습니다.
