NestJS는 내장된 종속성 주입을 사용해서 쉽게 테스트 코드를 작성할 수 있도록 도와준다! 종속성 주입
은 일반적으로 클래스가 아닌 인터페이스
를 기반으로하지만, TypeScript에서 인터페이스
는 런타임이 아닌 컴파일 타임에만 존재한다.(런타임에는 인터페이스는 증발함.) 그렇기 때문에 타입에 대한 100% 신뢰성을 부여하기 어렵다. NestJS에서는 클래스 기반 주입을 사용하는 것이 일반적이다.
import { Test, TestingModule } from '@nestjs/testing';
describe('UserService', () => {
let userService: UserService;
let userRepository: UserRepository;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [UserService, UserRepository],
}).compile();
userService = module.get<UserService>(UserService);
userRepository = module.get<UserRepository>(UserRepository);
});
}
기존에 Express에서는 sinon과 같은 패키지를 사용해서 테스트하려는 Service
를 모킹(Mocking)해서 사용했으며, 테스트 프레임워크를 직접 설치해서 사용했었지만 NestJS에서는 특정 도구를 강제하지는 않지만 JEST
를 기본 테스트 프레임워크로 제공해주며 테스팅 패키지도 제공하기 때문에 개발자가 다른 도구를 찾는데 소모하는 리소스를 줄일 수 있다.
NestJS에서 제공하는 @nestjs/testing
패키지를 사용하면 테스트에 사용되는 종속성만 선언해서 모듈을 만들고 해당 모듈로 UserService
, UserRepository
를 가져올 수 있다. 따로 단위테스트를 작성하기 위해서는 페이크 데이터를 만들어줄 필요가 있다. 기본적으로 들어갔다 나오는 결과물에 대한 것들을 지정해줘야 실제 테스팅이 이루어지기 때문이다. 물론 다른 프레임워크를 이용하면 데이터를 실제로 만들지 않아도 편리하게 할 수 있다.
const userRepositorySaveSpy = jest
.SpyOn(userRepository, 'save')
.mockResolvedValue(savedUser);
Jest에서는 모킹(Mocking)함수를 기본적으로 제공한다. 모킹은 단위테스트를 작성할 때. 해당 코드가 의존하는 부분을 가짜로 대체해서 만드는 것을 의미한다. 일반적으로는 테스트하려는 코드가 의존하는 부분을 직접 생성하기가 너무 부담스러울때 모킹이 사용된다. 위 코드에서는 SpyOn
으로 userRepository
의 save
함수를 호출하는 것을 Mock하고, 이 Mock된 함수는 mockResolvedValue
의 결괏값을 리턴하도록 하고 있다.
쉽게 말하면, userReposity에서 'save'를 call하는 것을 추적하면서, 결과값이 savedUser가 되도록하는 mocking함수라고 보면 된다.
이런식으로 Mocking함수에 넣어줄 실제 형태를 만들어서 삽입할 수 있다.
const savedUser: User = { firstName : 'brian', lastName: 'Kim' };
async updateUser(
id: number,
requestDto: UserUpdateRequestDto,
): Promise<User> {
const userToUpdate = await this.userRepository.findOne({where: { id: id },});
if(!userToUpdate) {
throw new BadRequestException(Message.NOT_FOUND_ERROR);
}
const { firstName, lastName, isActive);
user.update(firstName, lastName, isActive);
return this.userRepository.save(user);
}
Jest를 이용해서 위의 User 정보를 수정하는 서비스의 단위 테스트를 작성해보도록 하자!
UserService
의 updateUser
메서드를 테스트하려고하는데, 이 메서드에서는 두 가지를 테스트해야 한다. 유저 정보가 존재할 때는 성공적으로 수정하고 유저 정보가 존재하지 않을 경우엔 실패하는 로직에 대해서 검증이 필요하다.
describe('UserService', () => {
describe('유저 정보 수정', () => {
if('존재하지 않는 유저 정보를 수정할 경우 NotFoundError 발생시킨다.', async () => {
const userId: number = 1;
const updateUserDto: UpdateUserDto = {
firstName: 'John',
lastName: 'Park'
}
const userRepositoryFindOneSpy = jest
.spyOn(userRepository, 'findOne')
.mockResolvedValue(undefined);
try {
await userService.updateUser(userId, updateUserDto);
} catch(e) {
expect(e).toBeInstanceOf(NotFoundException);
expect(e.message).toBe(Message.NOT_FOUND_ERROR);
}
expect(userRepositoryFindOneSpy).toHaveBeenCalledWith({
where: {
id: userId,
},
});
});
});
})
위의 단위 테스트 코드에서는 존재하지 않는 유저의 정보를 수정할 때는 findOne
메서드가 null
값을 반환할 거라고 모킹해준다. updateUser
메서드는 가짜로 null
의 값이 반환되는 줄 알고 null
일 때, NotFoundError
가 발생하는 로직을 실행하게 된다.
expect(e).toBeInstanceOf(NotFoundException)
는 이 오류 메시지 객체가 NotFoundException
클래스의 인스턴스인지 확인하는 작업이며, .toBe
로 값을 비교해서 올바르게 오류 메시지가 나왔는지 검증할 수 있다.
describe('UserService', () => {
describe('유저 정보 수정', () => {
it('유저 정보를 성공적으로 수정한다.', async () => {
const userId: number = 1;
const updateUserDto: UpdateUserDto = {
firstName: 'John',
lastName: 'Park'
isActive: false,
};
const existingUser = User.of({
id: userId,
firstName: 'Brian',
lastName: 'Kim',
isActive: true,
});
const savedUser = User.of({
id: userId,
...updateUserDto,
});
const userRepositoryFindOneSpy = jest
.spyOn(userRepository, 'findOne')
.mockResolvedValue(existingUser);
const userRepositorySaveSpy = jest
.spyOn(userRepository, 'save')
.mockResolvedValue(savedUser);
const result = await userService.updateUser(userId, updateUserDto);
expect(userRepositoryFindOneSpy).toHaveBeenCalledWith({
where: {
id: userId,
},
});
expect(userRepositorySaveSpy).toHaveBeenCalledWith(savedUser);
expect(result).toEqual(savedUser);
});
});
})
updateUser
메서드에서 유저 정보를 찾아서 유저 정보를 정상적으로 수정했다는 로직의 테스트다. 미리 정의해놓은 existingUser
을 반환할 거라고 모킹 해주고, 이 반환된 값을 수정해서 저장하면 saveduser
을 반환할거라고 Mock했다. 그리고 오류가 없이 정상적으로 처리된 내용을 result
에 값을 담고, Jest의 expect
를 통해 검증한다. 먼저, .toHaveBeenCalledWith
는 Mocking 함수가 특정 인수로 호출되었는지 확인하는데 사용될 수 있고, .toEqual
로 개체의 모든 속성을 재귀적으로 비교한다.
UserService
의 User
정보를 수정하는 코드의 일부분을 살펴보았다.
E2E테스트는 (End to End) 종단테스트로 환경에 의존하지만, 단위 테스트는 기본적으로 환경이랑 상관없이 빠르게 실행되어야 한다. E2@테스트에서는 보통 테스트 데이터베이스를 사용하고, 테스트의 신뢰성이 높지만 속도가 느리다는 단점이 있다.