Nest.js에서는 기본적으로 testing tool은 jest를 사용하고 있으며, E2E(End To End) 테스트 파일을 만들어주고, 컨트롤러, 서비스 각각을 제너레이터를 사용하여 생성하면 유닛 테스트 파일도 자동으로 만들어줍니다.
아래의 원칙을 따를 경우 코드 품질이 훨씬 좋은 테스트 코드를 짤 수 있습니다.
FIRST 단위테스트 원칙
import { Test, TestingModule } from '@nestjs/testing';
import { UserService } from '../user/user.service';
describe('USerService', () => { // 테스트를 묘사..?
let service: UserService;
beforeEach(async () => { // 테스트하기전에 실행
const module: TestingModule = await Test.createTestingModule({
providers: [UserService],
}).compile();
service = module.get<UserService>(UserService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});
유닛테스트 시 mocking을 자주 사용합니다.
환경 구축을 위한 작업 시간이 많이 필요한 경우, 또는 테스트하려는 코드가 의존하는 부분을 직접 생성하기가 너무 부담스러울 때 Mock이 사용됩니다.
class MockService {
findOne(id: string): Promise<User | null> {
return null;
}
save(user: User): Promise<boolean> {
return true;
}
}
const moduleRef = await Test.createTestingModule({
providers: [
{
provide: UserService,
useClass: MockService,
},
],
}).compile();
const mockService = {
findOne: (id: string): Promise<User | null> => {
return null;
},
save: (user: User): Promise<boolean> => {
return true;
},
}.compile();
const moduleRef = await Test.createTestingModule({
providers: [
{
provide: UserService,
useValue: mockService,
},
],
}).compile();
it('should return true', async () => {
userService.findOne = jest.fn().mockResolvedValue(true); //(1)
jest.spyOn(userService, 'findOne').mockResolvedValue(true); //(2)
expect(await userService.findOne()).toTruthy();
});
import { UserService } from '../src/user/user.service';
...
const moduleRef = await Test.createTestingModule({
providers: [
UserService,
],
})
.useMocker(token => {
if (token === USerService) {
return {
findOne: jest.fn().mockResolvedValue(results)
};
}
type MockRepository<T = any> = Partial<Record<keyof Repository<T>, jest.Mock>>;
Repository를 Mocking 하기위해 Repository Type을 정의한 것
Partial : 타입 T의 모든 요소를 optional하게 한다.
Record : 타입 T의 모든 K의 집합으로 타입을 만들어준다.
keyof Repository : Repository의 모든 method key를 불러온다.
jest.Mock : 3번의 key들을 다 가짜로 만들어준다.
type MockRepository<T = any> : 이를 type으로 정의해준다.
import * as request from 'supertest';
import { Test } from '@nestjs/testing';
import { CatsModule } from '../../src/cats/cats.module';
import { CatsService } from '../../src/cats/cats.service';
import { INestApplication } from '@nestjs/common';
describe('Cats', () => {
let app: INestApplication;
let catsService = { findAll: () => ['test'] };
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
imports: [CatsModule],
})
.overrideProvider(CatsService)
.useValue(catsService)
.compile();
app = moduleRef.createNestApplication();
await app.init();
});
it(`/GET cats`, () => {
return request(app.getHttpServer())
.get('/cats')
.expect(200)
.expect({
data: catsService.findAll(),
});
});
afterAll(async () => {
await app.close();
});
});
테스트는 개발속도가 느리다는 단점이 있지만 리팩토링시 편리합니다. 현재 리팩토링 하고 있는 코드가 기존 시스템의 동작을 깨뜨리지 않을까하는 걱정을 하지 않아도 되며 잘 만든 테스트 코드는 소프트웨어의 품질을 높여줍니다. 또한 E2E테스트의 경우 CICD 과정에 테스트를 포함시키면 배포전 버그를 미리 파악할 수 있다는 장점이 있습니다.