(최종 프로젝트 진행중)
Nest.js에게는 ‘모듈’이라는 색다른 개념이 있어서 이 자체를 mocking하는 새로운 개념들을 익혀야 했다. 에러가 끊임없이 나서 안 그래도 부족한 Jest와 테스트 코드 지식에 난감함이 많았다.
※ 이하는 스스로 공부하며 적어둔 노트이며 불확실한 내용이 있을 수 있습니다. 학습용으로 적합하지 않음을 유념해주세요. ※
[Nest.js] Testing, 테스팅
[Nest.js] Testing, 테스팅
Jest가 기본 테스트 프레임워크로 제공된다.
필요 패키지 @nestjs/testing 를 설치해야 한다? ⇒ 기본 Nest.js 패키지에 이미 포함된 걸로 아는데.
Unit testing
For unit tests, the important one is theTestingModule.compile()
method.This method bootstraps a module with its dependencies (similar to the way an application is bootstrapped in the conventional
main.ts
file usingNestFactory.create()
), and returns a module that is ready for testing.
오오오…
import { Test, TestingModule } from '@nestjs/testing';
import { RequestsController } from './requests.controller';
import { RequestsService } from './requests.service';
describe('RequestsController', () => {
let controller: RequestsController;
let service: RequestsService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [RequestsController],
providers: [RequestsService],
}).compile();
controller = module.get<RequestsController>(RequestsController);
service = module.get<RequestsService>(RequestsService);
});
it('should be defined', () => {
expect(controller).toBeDefined();
expect(service).toBeDefined();
});
// "Isolated testing" (의존성을 가짜로 직접 주입해서 따로따로 테스트하기)
// beforeEach(() => {
// catsService = new CatsService();
// catsController = new CatsController(catsService);
// });
describe('findAll', () => {
it('shoul dreturn an array of requests', async () => {
const result = ['test'];
jest.spyOn(service, 'getRequests').mockImplementation(() => result);
expect(await controller.getRequests()).toBe(result);
});
});
});
⇒ .compile()은 비동기 함수임. = await으로 기다려야 함.
⇒ mock module로 만들 수 있는 건 controller와 service이다. Get 메소드로 얻으면 static instance가 되고, reseolve 메소드로 얻으면 …잘 모르겠는데 우선 ‘앱 전체에 걸쳐 하나의 모듈 service및 controller(이걸 뭐라고 부르더라? 싱글톤!)를 공유하게 된다’는 전제를 깬, 개별 인스턴스들이 된다고 한다.
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [RequestsController],
providers: [RequestsService],
}).compile();
controller = await module.resolve(RequestsController);
service = await module.resolve(RequestsService);
});
(참고: https://docs.nestjs.com/fundamentals/testing#testing-utilities)
the createTestingModule()
will need to be chained up with the useMocker()
method, passing a factory for your dependency mocks.
⇒ factory는 우선 함수인 것 같다.
Auto mocking
controller가 여러 provider를 주입받는데, 테스트할 떄 그 모든 재료를 준비하려면 힘들다. 그럴 때 ‘CatsService 하나에만 구체적으로 가짜 데이터로 테스트하는 mock을 만들려면’ = ‘만약 주입받은 provider가 CatsService면 이러이러한 가짜 데이터를 뱉는 가짜 함수를 가진 가짜 Service를 반환하고, CatsService가 아닌 다른 모든 provider라면 가짜 대행 moduleMocker이 반응하도록 내버려두려면’
// ...
import { ModuleMocker, MockFunctionMetadata } from 'jest-mock';
const moduleMocker = new ModuleMocker(global);
describe('CatsController', () => {
let controller: CatsController;
beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
controllers: [CatsController],
})
.useMocker((token) => {
const results = ['test1', 'test2'];
if (token === CatsService) {
return { findAll: jest.fn().mockResolvedValue(results) };
}
if (typeof token === 'function') {
const mockMetadata = moduleMocker.getMetadata(token) as MockFunctionMetadata<any, any>;
const Mock = moduleMocker.generateFromMetadata(mockMetadata);
return new Mock();
}
})
.compile();
controller = moduleRef.get(CatsController);
// 이런 '한 조각만 진심인' mock provider 역시도 container 바깥으로 호출될 수 있다.
moduleRef.get(CatsService)
});
});
This factory can take in an optional token, which is an instance token, any token which is valid for a Nest provider, and returns a mock implementation.
⇒ token은 우선 provider인 것 같다.
End-to-end testing
API endpoint를 테스트 해보는 게 바로 e2e, end-to-end 테스트임.
문서를 참고하여 일단 이렇게 작성해봤다: (아직 의문이 한가득)
// import * as request from 'supertest';
import request from 'supertest';
import { Test } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import { RequestsService } from './requests.service';
import { RequestsModule } from './requests.module';
describe('Requests', () => {
let app: INestApplication;
const getRequestsResult = [
{
request_id: 5,
reserved_time: '0000-00-00',
user: {
nickname: 'Nick',
cats: [],
},
},
{
request_id: 6,
reserved_time: '0000-00-00',
user: {
nickname: 'Nick',
cats: [],
},
},
];
const getRequestByIdResult = {
request_id: 10,
detail: '냥품 요청합니다!',
reserved_time: '2023-03-09',
user: {
nickname: 'Nick',
cats: [],
},
};
const createRequestResult = {}; // 특이사항 201
const updateRequestByIdResult = ''; // null?
const deleteRequestByIdResult = '';
let requestsService = {
getRequests: () => getRequestsResult,
getRequestById: (id) => getRequestByIdResult, // ? params로 받는 값을 어떻게 넣어주지.
createRequest: () => createRequestResult,
updateRequestById: () => updateRequestByIdResult,
deleteRequestById: () => deleteRequestByIdResult,
};
beforeAll(async () => {
const module = await Test.createTestingModule({
imports: [RequestsModule],
})
.overrideProvider(RequestsService)
.useValue(requestsService)
.compile();
app = module.createNestApplication();
await app.init();
});
it('/GET requests', () => {
return request(app.getHttpServer())
.get('/requests')
.expect(200)
.expect({ data: requestsService.getRequests() });
});
it('/GET requests/10', () => {
return request(app.getHttpServer())
.get('/requests/10')
.expect(200)
.expect({ data: requestsService.getRequestById() });
});
it('/POST requests', () => {
return request(app.getHttpServer())
.post('/requests')
.expect(201)
.expect({ data: requestsService.createRequest() });
});
it('/PATCH requests', () => {
return request(app.getHttpServer())
.patch('/requests/10')
.expect(200)
.expect({ data: requestsService.updateRequestById() });
});
it('/DELETE requests', () => {
return request(app.getHttpServer())
.delete('/requests')
.expect(200)
.expect({ data: requestsService.deleteRequestById() });
});
afterAll(async () => {
await app.close();
});
});
오늘 작성해본 (튜터님 붙잡고 에러를 해결한) Unit test code 조각:
오늘 작성해본 (튜터님 붙잡고 에러를 해결한) Unit test code 조각:
// messages.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { User } from 'src/users/user.entity';
import { Message } from './message.entity';
import { MessagesRepository } from './messages.repository';
import { MessagesService } from './messages.service';
const mockMessagesRepository = () => {
return {
getMessageById: jest.fn(),
};
};
// 이런식으로 필요한 Entity 샘플을 하나하나 다 구현해줘야 한다고 한다..!
// 사실 이것보다 더 정교하게 해야 한다고... 그래서 쿼리를 한 번 날려서 실제 값을 가져오는 방식을 취한다고.
const user: User = {
user_id: 1,
address: '서울시 성북구',
email: '',
nickname: 'Nick',
name: 'name',
phone_number: '',
password: '',
status: '가입 대기',
created_at: new Date(),
updated_at: new Date(),
deleted_at: null,
hashdRt: null,
cat_likes: [],
cats: [],
share_comments: [],
post_comments: [],
posts: [],
receive_messages: [],
referral_code: '',
requests: [],
send_messages: [],
share_posts: [],
share_products: [],
target_user_likes: [],
user_likes: [],
};
const message: Partial<Message> = {
message_id: 1,
sender_id: 2,
recipient_id: 3,
content: '냥품 지원합니다!',
// created_at: new Date(),
// deleted_at: null,
// send_user: user,
// receive_user: user,
};
describe('MessagesService', () => {
let service: MessagesService;
let repository: jest.Mocked<MessagesRepository>;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
MessagesService,
{
provide: MessagesRepository,
useFactory: mockMessagesRepository,
},
],
}).compile();
service = module.get<MessagesService>(MessagesService);
repository = module.get(MessagesRepository);
});
describe('getMessageById', () => {
it('should call repository', async () => {
// const spy = jest.spyOn(repository, 'getMessageById').mockResolvedValue(message);
repository.getMessageById.mockResolvedValue(message);
expect(await service.getMessageById(1)).toStrictEqual(message);
expect(repository.getMessageById).toBeCalledWith(1);
});
});
});
⇒ 핵심1: 필요한 Entity 타입마다 모든 컬럼을 다 포함한 샘플 객체를 만들어야 한다는 것. User와 연결된 모든 Enitty는 User와 문어발로 연결된 모든 다른 Entity를 mocking해야 함을 의미한다..
⇒ 핵심2: spyOn은 너무 깊은(?) 테스트 도구이다. 단순한 (얕은) 테스트에서라면 그러지 말고 repository.getMessageById처럼 곧바로 repository 인스턴스를 사용하는 방식을 사용하라고 한다.
⇒ 핵심3: TypeORM 기본 세팅으로 냅두면 Message 인스턴스에 8개 컬럼을 모두 넣어야만 타입 체크가 mockResolvedValue(message)에서 통과되었다. 이것을 의도한 바대로 몇 개의 컬럼만 제대로 받아오는지를 테스트하려면,
// messages.repository.ts
import { Injectable } from '@nestjs/common';
import { DataSource, Repository } from 'typeorm';
import { Message } from './message.entity';
@Injectable()
export class MessagesRepository extends Repository<Message> {
constructor(private dataSource: DataSource) {
super(Message, dataSource.manager);
}
async getMessageById(id: number): Promise<Partial<Message>> {
const message = await this.createQueryBuilder('m')
.select(['a.message_id', 'a.sender_id', 'a.recipient_id', 'content'])
.where({ message_id: id })
// .relation('a.sender_user')
.getOne();
return message;
}
}
⇒ 이렇게 해당 repository 메소드의 반환 타입을 partial Message로 직접 지정해줘야 한다. 안 그러면 자동적으로 (whole) Message로 타입이 지정되어서, 정작 불러오는 컬럼은 저렇게 4개 뿐인 게 맞는데 타입은 8개 컬럼이 있는 (whole) Message로 지정이 되어서, 테스트하려고 할 때 무작정 8개 컬럼을 모두 넣어야만 합당한 Message 타입으로 통과할 수 있게 된다.
결론: repository 메소드의 반환타입도 Partial, spec.ts 테스트 코드에서 작성하는 mock Entity도 타입을 Partial로 지정해주면, 원활히 ‘그 중 몇 개의 컬럼만 반환된 Entity 객체’의 경우를 테스트할 수 있게 된다.
마지막으로 테스트 대상인 service 코드이다:
// messages.service.ts
import { Injectable } from '@nestjs/common';
import { MessagesRepository } from './messages.repository';
@Injectable()
export class MessagesService {
constructor(private readonly repository: MessagesRepository) {}
async getMessageById(id: number) {
return await this.repository.getMessageById(id);
}
}
Uploaded by N2T