import { NotFoundException } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import { ErrorType } from '../../../common/type/Message.type';
import { Repository } from 'typeorm';
import { User } from '../entities/user.entity';
import { UserService } from '../user.service';
type MockRepository<T = any> = Partial<Record<keyof Repository<T>, jest.Mock>>;
const user = {
email: 'jhyeom1545@gmail.com',
name: '홍길동',
points: 500,
password: '12345',
createdAt: new Date('2022-10-11T18:47:32.165Z'),
updatedAt: new Date('2022-10-11T18:47:32.165Z'),
deletedAt: null,
};
const mockRepository = () => ({
save: jest.fn(),
findOne: jest.fn(),
softDelete: jest.fn(),
});
describe('UserService', () => {
let userService: UserService;
let userRepository: MockRepository<User>;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [
UserService,
{
provide: 'UserRepository',
useFactory: mockRepository,
},
],
}).compile();
userService = module.get<UserService>(UserService);
userRepository = module.get('UserRepository') as MockRepository<User>;
});
it('유저 서비스 toBeDefined 테스트', () => {
expect(userService).toBeDefined();
});
describe('findOne', () => {
it('유효한 email일 경우 user를 반환합니다.', async () => {
jest.spyOn(userRepository, 'findOne').mockResolvedValue(user);
const validUser = 'jhyeom1545@gmail.com';
const result = await userService.findOne({ email: validUser });
expect(result).toEqual(user);
});
it('유효하지 않은 email일 경우 예외를 발생시킵니다.', async () => {
jest.spyOn(userRepository, 'findOne').mockResolvedValue(undefined);
await expect(async () => {
await userService.findOne({ email: 'isnotValid@naver.com' });
}).rejects.toThrowError(new NotFoundException(ErrorType.user.msg));
});
});
});
유저 서비스의 findOne부분만을 작성한 테스트 코드는 다음과 같습니다.
type MockRepository<T = any> = Partial<Record<keyof Repository<T>, jest.Mock>>;
keyof Repository로 해당 레포지토리가 가지고 있는 메서드를 추출한 다음, Partial
로 감싸 optional 처리를 진행해줍니다.
Partial Type, Pick Type, Omit Type
Partial Type: 파셜 타입은 특정 타입의 부분 집합을 만족하는 타입을 정의할 수 있습니다.
Pick Type: 픽 타입은 특정 타입에서 몇 개의 속성을 선택하여 타입을 정의합니다.
Omit Type: 특정 속성만 제거한 타입을 정의합니다. pick의 반대typeof : 객체 데이터를 객체 타입으로 변환해주는 연산자
async findOne({ email }: { email: string }): Promise<User> {
const result = await this.userRepository.findOne({
where: { email },
});
// 이메일 존재하는지 확인
if (!result) throw new NotFoundException(ErrorType.user.msg);
return result;
}
User.service.ts 코드의 findOne은 찾는 결과가 없을 경우 NotFountException을 발생시킵니다.
describe('findOne', () => {
it('유효한 email일 경우 user를 반환합니다.', async () => {
jest.spyOn(userRepository, 'findOne').mockResolvedValue(user);
const validUser = 'jhyeom1545@gmail.com';
const result = await userService.findOne({ email: validUser });
expect(result).toEqual(user);
});
it('유효하지 않은 email일 경우 예외를 발생시킵니다.', async () => {
jest.spyOn(userRepository, 'findOne').mockResolvedValue(undefined);
await expect(async () => {
await userService.findOne({ email: 'isnotValid@naver.com' });
}).rejects.toThrowError(new NotFoundException(ErrorType.user.msg));
});
});
테스트코드는 다음과 같이 작성하였습니다.
it('유효한 email일 경우 user를 반환합니다.', async () => {
jest.spyOn(userRepository, 'findOne').mockResolvedValue(user);
const validUser = 'jhyeom1545@gmail.com';
const result = await userService.findOne({ email: validUser });
expect(result).toEqual(user);
});
앞선 전체 코드에서 mockRepository의 findOne을 jest.fn()으로 바꿔 주었습니다.
그리고 mockResolvedValue를 통해서 await userRepository.findOne의 값이 user를 가지도록 만들어 주었습니다.
이렇게 작성하였을 경우, userService.findOne을 실행했을 때, user를 반환합니다.
jhyeom1545@gmail.com의 값은 존재하는 값이기 때문에 user를 반환하고 테스트가 성공합니다.
it('유효하지 않은 email일 경우 예외를 발생시킵니다.', async () => {
jest.spyOn(userRepository, 'findOne').mockResolvedValue(undefined);
await expect(async () => {
await userService.findOne({ email: 'isnotValid@naver.com' });
}).rejects.toThrowError(new NotFoundException(ErrorType.user.msg));
});
// ErrorType.user.msg === 유효하지 않은 이메일입니다.
userRepository.findOne의 값이 undefined의 값을 갖도록 mockValue를 넣어 주었다.
User.service.findOne의 result가 존재하지 않을 경우 NotFoundException이 발생 하기 때문에,
expect를 통해 userService.findOne의 예상 결과가 NotFoundException('유효하지 않은 이메일입니다.')을 반환할 것임을 알 수 있습니다.
rejects는 Promise에 Error가 발생할 경우 사용됩니다.