NestJS에서 DB조회를 하기 위해 자주 쓰이는 createQueryBuilder, 그리고 그 이후 연결되는 chained method들을 mocking하는 방법에 대해 정리해보고자 한다.
테스트 대상
오늘 테스트 대상은 아래와 같다.
mrlogRepository 라는 곳에서 데이터를 원하는 형식으로 뽑아오는 간단한 메소드이다.
해당 메소드를 만들 때에는 딱히 어려움이 없었지만, 테스트 코드를 만들 때 꽤나 골치가 아팠다.
export class MrLogService {
constructor(
@InjectRepository(MRLog)
private mrlogRepository: Repository<MRLog>,
) {}
async dashboardMrLog(): Promise<any> {
const [latest2H, now] = getTimeRange('test');
// const [latest2H, now] = getTimeRange();
const data = await this.mrlogRepository
.createQueryBuilder()
.select('HOUR(recv_dt) AS HOUR')
.addSelect('FLOOR(MINUTE(recv_dt)/10)*10 + 10 AS MINUTE')
.addSelect('COUNT(recv_dt) AS CNT')
.addSelect('COUNT(if(mbr_type="1VInconsistent", 1, null)) AS type1')
.addSelect('COUNT(if(mbr_type="Implausible", 1, null)) AS type2')
.addSelect('COUNT(if(mbr_type="ObsImplausible", 1, null)) AS type3')
.where('recv_dt BETWEEN :latest2H AND :now', { latest2H, now })
.groupBy('HOUR(recv_dt), FLOOR(MINUTE(recv_dt)/10)*10 +10')
.getRawMany();
return data;
}
}
테스트 방법
어떤 환경에서 테스트하든지, 테스트 대상과 같은 환경을 만들어 주는 것은 중요하지만, NestJS에서는 더더욱 중요하다.
일반적으로 NestJS에서 테스트 할 때 어떻게 dependency들을 inject하여 같은 환경을 만들어 줄 수 있는지에 대해서는 이미 자세히 작성한 링크를 참고하자.
1. 같은 환경 만들어 주기
import { Test } from '@nestjs/testing';
import { Repository } from 'typeorm';
import { MrLogService } from './mr-log.service';
import { MRLog } from '../entities/mrlog.entity';
import { getRepositoryToken } from '@nestjs/typeorm';
//2. MockRepository 타입을 선언한다.
type MockRepository<T = any> = Partial<Record<keyof Repository<T>, jest.Mock>>;
//3. mockRespository 를 생성한다.
const mockRepository = {
createQueryBuilder: jest.fn().mockReturnValue({
select: jest.fn().mockReturnThis(),
addSelect: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
groupBy: jest.fn().mockReturnThis(),
getRawMany: jest.fn().mockReturnValue([]),
}),
};
describe('mrLog Chart', () => {
let mrlogService: MrLogService;
let mrlogRepository: MockRepository<MRLog>;
//1. 테스팅 모듈 생성
//beforeEach 메소드는 테스트블록이 실행되기 직전에 매번 실행된다.(초기화 목적)
beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
//4. 같은환경 조성
//테스트 대상인 MrLogService와 그 dependency인 mrlogRespository를 mockRepository를 이용하여 providers로 등록한다
providers: [
MrLogService,
{ provide: getRepositoryToken(MRLog), useValue: mockRepository },
],
}).compile();
mrlogService = moduleRef.get<MrLogService>(MrLogService);
mrlogRepository = moduleRef.get(getRepositoryToken(MRLog));
});
일반적인 테스팅 코드를 작성하는 방법과 동일하다.
단, 여기에서 주의깊게 볼 필요가 있는 부분은 MockRepository 타입 선언방식과 mockRepository를 생성하고 등록하는 부분이다.
자, 하나씩 뜯어가면서 왜 저렇게 하는지 알아보자.
1. MockRepository 타입 선언방식
type MockRepository<T = any> = Partial<Record<keyof Repository<T>, jest.Mock>>;
keyof Repository<T>를 통해 해당 레포지토리가 가지고 있는 메소드들을 추출하고,
Record<keyof Repository<T>, jest.Mock> 를 통해, key 타입은 아까 추출한 타입으로, value의 타입은 jest.Mock으로 지정
Partial<Record<keyof Repository<T>, jest.Mock>> 를 통해, 정의 된 메소드의 일부를 사용 할 것이기 떄문에, optional로 처리
즉, 위의 코드는 type MockRepository는 객체 형식을 띄게 되는데, 이때 key 타입은 Repository<T>가 가지고 있는 키타입, value의 타입은 jest.Mock타입이 된다는 뜻이다.
2. mockRepository를 생성
const mockRepository = {
createQueryBuilder: jest.fn().mockReturnValue({
select: jest.fn().mockReturnThis(),
addSelect: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
groupBy: jest.fn().mockReturnThis(),
getRawMany: jest.fn().mockReturnValue([
{
HOUR: 2,
MINUTE: 30,
CNT: '16',
type1: '0',
type2: '16',
type3: '0',
},
]),
}),
};
mockRepository를 생성하고, 그 안에 내가 필요한 createQueryBuilder, select, etc 같은 여러 메소드 들을 등록해줬다.
이때, 우리가 일반적으로 mock객체를 생성하고 메소드를 등록할 때처럼 단순히 jest.fn()으로 끝내는 것이 아니라, mockReturnThis()를 통해서, chaining되는 함수를 mocking 할 수 있다.
즉, mockRepository는 createQueryBuilder라는 하나의 메소드를 가지게 되고, createQueryBuilder는 다양한 메소드 들을 리턴하는 함수가 되게 되는 것이다. 그리고 그 다양한 함수들은 체이닝이 가능하다는 것을 의미하게 된다.
mockReturnThis()
methods의 chaining이 일어나게 되는 경우를 위해, 항상 this를 리턴해주는 메소드이다.
즉, 아래의 두 myMethod는 같은 뜻이다. ( 참고링크 )
const myObj = {
myMethod: jest.fn().mockReturnThis(),
};
// is the same as
const otherObj = {
myMethod: jest.fn(function () {
return this;
}),
};
2. mockRepository를 등록
자, 이제 아까 만들어놨던 MockRepository 타입을 이용하여, mrlogRepository를 등록 해보자
describe('mrLog Chart', () => {
let mrlogService: MrLogService;
//1. mrlogRepository를 MockRepository<MRLog>로 선언
let mrlogRepository: MockRepository<MRLog>;
beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
providers: [
MrLogService,
//2. getRepositoryToken() is a helper method that allows you to get the same injection token that @InjectRepository() returns.
{ provide: getRepositoryToken(MRLog), useValue: mockRepository },
],
}).compile();
mrlogService = moduleRef.get<MrLogService>(MrLogService);
mrlogRepository = moduleRef.get(getRepositoryToken(MRLog));
});
우리가 테스트 할려고 했던 대상에서 아래와 같이 dependency를 inject하고 있었다.
@InjectRepository(MRLog)
private mrlogRepository: Repository<MRLog>,
하지만, 우리는 단순한 Unit Test를 하는 것이고, 실제 DB에 영향을 주고 싶지 않다. 이럴 때 getRepositoryToken을 사용하면,
@InjectRepository(MRLog)가 리턴하는 값과 같은 injection token을 가질 수 있고, 이를 통해 우리는 실제 DB와 연결하지 않고도,
테스트 대상과 같은 환경을 만들어 줄 수 있는 것이다.
'테스트코드' 카테고리의 다른 글
[Solved] Jest did not exit one second after the test run has completed. (0) | 2022.08.04 |
---|---|
테스트 DB와 연결하기(feat. Jest with NestJS) (0) | 2022.04.08 |
Unit Test에서 독립성 유지하기(feat. NestJS with Jest) (0) | 2022.03.28 |
NestJS에서 테스트코드 작성(feat. Jest, @nestjs/testing) (1) | 2022.03.28 |