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 });