Project Log
Node.js백 엔드 서버 개발자 주니어의 동영상 쇼츠 서비스 프로젝트 !! EP03
Jcob.moon
2024. 11. 19. 22:28
import { Test, TestingModule } from '@nestjs/testing';
import { CommentService } from './comment.service';
import { CommentEntity } from './entities/comment.entity';
import { VideoEntity } from 'src/video/entities/video.entity';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { UserEntity } from 'src/user/entity/user.entity';
import { BadRequestException, NotFoundException } from '@nestjs/common';
import { CommentDto } from './dto/comment.dto';
describe('CommentService', () => {
let service: CommentService;
let commentRepository: Repository<CommentEntity>;
let videoRepository: Repository<VideoEntity>;
let userRepository: Repository<UserEntity>;
const mockCommentRepository = {
create: jest.fn(),
save: jest.fn(),
find: jest.fn(),
findOne: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
findOneBy: jest.fn(),
};
const mockVideoRepository = {
find: jest.fn(),
};
const mockUserRepository = {
findOne: jest.fn(),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
CommentService,
{
provide: getRepositoryToken(CommentEntity),
useValue: mockCommentRepository,
},
{
provide: getRepositoryToken(VideoEntity),
useValue: mockVideoRepository,
},
{
provide: getRepositoryToken(UserEntity),
useValue: mockUserRepository,
},
],
}).compile();
service = module.get<CommentService>(CommentService);
commentRepository = module.get<Repository<CommentEntity>>(getRepositoryToken(CommentEntity));
videoRepository = module.get<Repository<VideoEntity>>(getRepositoryToken(VideoEntity));
});
describe('댓글 생성', () => {
it('댓글 생성 성공 검증', async () => {
const commentDto: CommentDto = { content: 'test' };
const user: UserEntity = { id: 1 } as UserEntity;
const videoId = 1;
mockCommentRepository.create.mockResolvedValue({
userId: user.id,
content: commentDto.content,
});
mockCommentRepository.save.mockResolvedValue({
id: 1,
userId: user.id,
content: commentDto.content,
});
const result = await service.createComment(commentDto, user, videoId);
expect(mockCommentRepository.create).toHaveBeenCalledWith({
userId: user.id,
content: commentDto.content,
commentGroup: 1,
depth: 0,
orderNumber: 1,
parentComment: 0,
});
expect(mockCommentRepository.save).toHaveBeenCalled();
expect(result).toEqual({
userId: user.id,
content: commentDto.content,
});
});
});
describe('댓글 목록 조회', () => {
it('댓글 목록 조회 성공 검증', async () => {
const videoId = 1;
const comments = [
{
id: 1,
content: 'content',
userId: 1,
createdAt: new Date(),
},
];
mockVideoRepository.find.mockResolvedValue({
where: { id: videoId },
});
mockCommentRepository.find.mockResolvedValue(comments);
const result = await service.findAll(videoId);
expect(mockCommentRepository.find).toHaveBeenCalledTimes(1);
expect(result).toEqual({ data: comments });
});
it('댓글 목록 조회 실패 시 NotFoundException 출력 검증', async () => {
mockVideoRepository.find.mockResolvedValue(null);
await expect(service.findAll(1)).rejects.toThrow(NotFoundException);
});
});
describe('댓글 상세 조회', () => {
it('댓글 상세 조회 성공 검증', async () => {
const videoId = 1;
const commentId = 1;
mockVideoRepository.find.mockResolvedValue({ id: videoId });
mockCommentRepository.findOne.mockResolvedValue({
id: commentId,
userId: 1,
content: 'test',
createdAt: '2030-12-12',
});
const result = await service.findOne(videoId, commentId);
expect(mockVideoRepository.find).toHaveBeenCalledWith({
where: { id: videoId },
});
expect(mockCommentRepository.findOne).toHaveBeenCalledWith({
where: { id: commentId },
});
expect(result).toEqual({
id: commentId,
userId: 1,
content: 'test',
createdAt: '2030-12-12',
});
});
});
describe('댓글 수정 ', () => {
it('댓글 수정 성공 검증', async () => {
const videoId: number = 1;
const userId: UserEntity = { id: 1 } as UserEntity;
const commentId: CommentEntity = { id: 1 } as CommentEntity;
const content: CommentDto = { content: 'content' } as CommentDto;
const updatedComment = {
id: 1,
content: 'test',
userId: 1,
createdAt: '2030-12-12',
};
mockCommentRepository.findOneBy.mockResolvedValue({ userId, commentId });
mockCommentRepository.update.mockResolvedValue({ commentId, content });
const result = await service.updateComment(videoId, commentId.id, content, userId);
expect(result).toEqual(updatedComment);
});
it('댓글 수정 실패 시 NotFoundException 출력 검증', async () => {
const comment: CommentDto = { content: 'test' } as CommentDto;
const user: UserEntity = { id: 1 } as UserEntity;
mockCommentRepository.findOneBy.mockResolvedValue(null);
await expect(service.updateComment(1, 1, comment, user)).rejects.toThrow(NotFoundException);
});
});
describe('댓글 삭제', () => {
it('댓글 삭제 성공 검증', async () => {
const videoId = 1;
const commentId = 1;
const user = { id: 1 } as UserEntity;
mockCommentRepository.findOneBy.mockResolvedValue({
id: commentId,
userId: user.id,
});
mockCommentRepository.delete.mockResolvedValue({
affected: 1,
});
const result = await service.removeComment(videoId, commentId, user);
expect(mockCommentRepository.findOne).toHaveBeenCalledWith({
where: { id: commentId },
});
expect(mockCommentRepository.delete).toHaveBeenCalledWith({
id: commentId,
});
expect(result).toEqual({
success: true,
message: '댓글이 성공적으로 삭제 되었습니다.',
});
});
it('댓글 삭제 실패', async () => {
const videoId = 1;
const commentId = 1;
const user = { id: 1 } as UserEntity;
mockCommentRepository.findOneBy.mockResolvedValue({
id: commentId,
userId: user.id,
});
mockCommentRepository.delete.mockResolvedValue({
affected: 0,
});
await expect(service.removeComment(1, 1, user)).rejects.toThrow(BadRequestException);
});
});
});
import { Test, TestingModule } from '@nestjs/testing';
import { LikeService } from './like.service';
import { getRepositoryToken } from '@nestjs/typeorm';
import { LikeEntity } from './entities/like.entity';
import { Repository } from 'typeorm';
describe('LikeService', () => {
let likeService: LikeService;
let likeRepository: Repository<LikeEntity>;
// const mockLikeRepository = {
// findOne: jest.fn(),
// delete: jest.fn(),
// save: jest.fn(),
// };
const mockLike = { id: 1, user: { id: 1 }, video: { id: 123 } };
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
LikeService,
{
provide: getRepositoryToken(LikeEntity),
useValue: {
// ...mockLikeRepository,
findOne: jest.fn(),
delete: jest.fn(),
save: jest.fn(),
},
},
],
}).compile();
likeService = module.get<LikeService>(LikeService);
likeRepository = module.get<Repository<LikeEntity>>(getRepositoryToken(LikeEntity));
});
describe('toggleLike', () => {
it('이미 좋아요 기록이 있다면 좋아요를 삭제한다', async () => {
// mockLikeRepository.findOne.mockResolvedValue(mockLike);
// mockLikeRepository.delete.mockResolvedValue({ affected: 1 });
jest.spyOn(likeRepository, 'findOne').mockResolvedValue(mockLike as LikeEntity);
jest.spyOn(likeRepository, 'delete').mockResolvedValue({ affected: 1 } as any);
const result = await likeService.toggleLike(123, 1);
expect(likeRepository.findOne).toHaveBeenCalledWith({
where: { video: { id: 123 }, user: { id: 1 } },
});
expect(likeRepository.delete).toHaveBeenCalledWith({
video: { id: 123 },
user: { id: 1 },
});
expect(result).toEqual({ affected: 1 });
});
it('좋아요 기록이 없다면 좋아요를 저장한다.', async () => {
// mockLikeRepository.findOne.mockResolvedValue(null);
// mockLikeRepository.save.mockResolvedValue(mockLike);
jest.spyOn(likeRepository, 'findOne').mockResolvedValue(null);
jest.spyOn(likeRepository, 'save').mockResolvedValue(mockLike as LikeEntity);
const result = await likeService.toggleLike(123, 1);
expect(likeRepository.findOne).toHaveBeenCalledWith({
where: {
video: { id: 123 },
user: { id: 1 },
},
});
expect(likeRepository.save).toHaveBeenCalledWith({
video: { id: 123 },
user: { id: 1 },
});
expect(result).toEqual(mockLike);
});
});
});
코멘트 서비스 테스트 코드에서 오늘 배웠던 내용들을 적어보겠다 .
mockReturnValue vs mockResolvedValue:
- mockReturnValue: 일반적인 동기 함수에 사용됩니다.
- mockResolvedValue: 비동기 함수에서 Promise.resolve와 같이 작동하며, 비동기 결과를 설정할 때 사용됩니다.
그리고 toHAveBenncalledWith() 을 자주써서 관련해서 조사 해보았다.
이런식으로 expect 뒤에 사용 할수있다
const mockFunction = jest.fn();
mockFunction('firstCall');
mockFunction('secondCall');
// 1. 함수가 호출되었는지 확인
expect(mockFunction).toHaveBeenCalled();
// 2. 호출 횟수 검증
expect(mockFunction).toHaveBeenCalledTimes(2);
// 3. 호출 인자 검증
expect(mockFunction).toHaveBeenCalledWith('firstCall');
expect(mockFunction).toHaveBeenLastCalledWith('secondCall');
// 4. N번째 호출 시 인자 검증
expect(mockFunction).toHaveBeenNthCalledWith(1, 'firstCall');
expect(mockFunction).toHaveBeenNthCalledWith(2, 'secondCall');
프로젝트를 진행중에 좋아요 기능관련된 테스트 코드를 진행하며
Jest.spyOn 과 jest.fn() 을 통한 모킹 방법에 대한 차이를 조금 더 알고 싶어 졌고
두개 다 작동은 하나 그 차이에 대해서 그리고 우리 프로젝트에 어떤것이 어울린다고 생각하고
어떤것을 사용했는지 적어야겠다.
1. jest.spyOn
목적
- jest.spyOn은 특정 객체의 메서드를 감시(spying)하거나, 해당 메서드의 동작을 부분적으로 제어(mocking)하는 데 사용됩니다.
- 객체의 나머지 메서드나 속성은 원래 동작을 유지합니다.
작동 방식
- 대상 객체의 특정 메서드를 감시하여 호출 여부, 호출 횟수, 인자 등을 추적합니다.
- 선택적으로 해당 메서드의 구현을 교체(mocking)할 수 있습니다.
2. 레포지토리/모듈 전체를 mocking
목적
- 레포지토리(모듈) 전체를 mocking하는 경우, 해당 모듈이 제공하는 모든 함수, 메서드, 혹은 값들을 가짜로 대체합니다.
- 실제 동작은 완전히 차단되며, 원하는 구현(mock)으로 대체됩니다.
작동 방식
- Jest의 jest.mock()을 사용하여 특정 모듈을 대체합니다.
- 모듈 내부의 모든 함수와 객체를 가짜(mocked) 버전으로 제공하며, 이를 기반으로 테스트를 수행합니다.
또한 findOne 과 findOneby 에 차이가 있는데
1. findOne 사용 시:
- 복잡한 조회가 필요하거나 연관 관계를 포함해야 하는 경우.
const result = await userRepository.findOne({
where: { id: 1 },
relations: ['profile', 'posts'], // 연관된 테이블 로드
order: { createdAt: 'DESC' }, // 정렬 조건
});
2. findOneBy 사용 시:
- 간단히 특정 조건으로 데이터를 조회하는 경우.
const result = await userRepository.findOneBy({ id: 1 });