개인 프로젝트/Toy & Side

[TOY] 개발 - 단위 테스트(domain, service)

kwang2134 2024. 12. 2. 17:24
728x90
반응형
728x90

이전 진행 상황

  • Service 개발(Comment, Photo)
  • PostService 추천/비추천 로직 변경 -> 회원, 비회원(세션 단위) 24시간 이내 추천/비추천 제한
  • 추천/비추천 로직 변경으로 인한 기술 스택 추가 -> Redis

  • User
  • Post
  • Comment
  • Photo

User

user 도메인의 단위 테스트로 UserService의 로직을 검증하는 테스트가 추가되었습니다. 데이터베이스와 상호 작용 없이 순수 로직 테스트로 Mock 라이브러리가 사용되었고 login 메서드에 대한 테스트가 수행되었습니다.


추가 사항

  • UserServiceTest 추가

class UserServiceTest

UserService에 대한 단위 테스트입니다. 로그인 메서드가 각 케이스별로 테스트되었습니다.

@Slf4j
@ExtendWith(MockitoExtension.class)
class UserServiceTest {

    @Mock
    UserRepository repository;

    @InjectMocks
    UserService service;

    static final String LOGIN_ID = "asdf1234";
    static final String PASSWORD = "1234";
    User user;

    @BeforeEach
    void setUp() {
        user = User.builder()
                .id(1L)
                .loginId(LOGIN_ID)
                .password(PASSWORD)
                .role(Role.USER)
                .build();
    }

    @Test
    void login_Success() {
        // given
        when(repository.findByLoginId(LOGIN_ID)).thenReturn(Optional.of(user));

        // when
        User loginUser = service.login(LOGIN_ID, PASSWORD);

        // then
        assertThat(loginUser).isEqualTo(user);
        verify(repository).findByLoginId(LOGIN_ID);
    }

    @Test
    void login_FailedWrongId() {
        // given
        when(repository.findByLoginId("wrong")).thenReturn(Optional.empty());

        // when & then
        assertThatThrownBy(() -> service.login("wrong", PASSWORD)).isInstanceOf(InvalidLoginException.class);
    }

    @Test
    void login_FailedWrongPassword() {
        // given
        when(repository.findByLoginId(LOGIN_ID)).thenReturn(Optional.of(user));

        // when & then
        assertThatThrownBy(() -> service.login(LOGIN_ID, "wrong")).isInstanceOf(InvalidLoginException.class);
    }
}

Spring Security를 사용함에 따라 순수 login 기능 자체는 사용되지 않을 예정이나 기능 확장 시 사용될 사용될 가능성을 염두해 테스트를 진행하였습니다. 


Post

PostService의 단위 테스트로 추천/비추천 기능이 Redis에 의존하게 되며 간단한 게시글 타입 변경, 인기글 조회에 간한 메서드만 테스트하였습니다.


추가 내용

  • PostServiceTest 추가

class PostServiceTest

@Slf4j
@ExtendWith(MockitoExtension.class)
class PostServiceTest {

    @Mock
    PostRepository repository;

    @InjectMocks
    PostService service;

    static final long POST_ID = 1L;
    Post post;

    @BeforeEach
    void setUp() {
        post = Post.builder()
                .id(POST_ID)
                .title("title")
                .content("content")
                .postType(PostType.NORMAL)
                .recomCount(0)
                .notRecomCount(0)
                .build();
    }

    @Test
    void changeToNormal() {
        //given
        post.changeTypeNotice();
        when(repository.findById(POST_ID)).thenReturn(Optional.of(post));

        //when
        service.changeToNormal(POST_ID);

        //then
        assertThat(post.getPostType()).isEqualTo(PostType.NORMAL);
        verify(repository).findById(POST_ID);
    }

    @Test
    void changeToNotice() {
        //given
        when(repository.findById(POST_ID)).thenReturn(Optional.of(post));

        //when
        service.changeToNotice(POST_ID);

        //then
        assertThat(post.getPostType()).isEqualTo(PostType.NOTICE);
        verify(repository).findById(POST_ID);
    }

    @Test
    void changeToPopular() {
        //given
        when(repository.findById(POST_ID)).thenReturn(Optional.of(post));

        //when
        service.changeToPopular(POST_ID);

        //then
        assertThat(post.getPostType()).isEqualTo(PostType.POPULAR);
        verify(repository).findById(POST_ID);
    }

    @Test
    void viewPopularPosts() {
        // given
        post.changeTypePopular();
        List<Post> popularPosts = List.of(post);
        when(repository.findByPostType(PostType.POPULAR)).thenReturn(popularPosts);

        // when
        List<Post> result = service.viewPopularPosts();

        // then
        assertThat(result).hasSize(1);
        assertThat(result.get(0).getPostType()).isEqualTo(PostType.POPULAR);
        verify(repository).findByPostType(PostType.POPULAR);
    }
}

Comment

comment에 관한 단위 테스트로 댓글 작성에 관해 중점으로 테스트를 진행하였습니다. 댓글 작성의 경우 부모 댓글의 유무로 원본 댓글인지 대댓글인지 판별하게 되고 작성 로직이 달라지는 과정을 테스트하였습니다.

@Slf4j
@ExtendWith(MockitoExtension.class)
class CommentServiceTest {

    @Mock
    CommentRepository repository;

    @Mock
    PostRepository postRepository;

    @InjectMocks
    CommentService service;

    static final long USER_ID = 1L;
    static final Long POST_ID = 1L;
    static final Long PARENT_ID = 1L;
    static final Long COMMENT_ID = 2L;

    Comment comment;
    Comment parentComment;
    User user;
    Post post;

    @BeforeEach
    void setUp() {
        user = User.builder()
                .id(USER_ID)
                .loginId("LOGIN_ID")
                .password("PASSWORD")
                .role(Role.USER)
                .build();

        post = Post.builder()
                .id(POST_ID)
                .title("post title")
                .content("post content")
                .postType(PostType.NORMAL)
                .recomCount(0)
                .notRecomCount(0)
                .user(user)
                .build();

        parentComment = Comment.builder()
                .id(PARENT_ID)
                .content("parent content")
                .user(user)
                .post(post)
                .build();

        comment = Comment.builder()
                .id(COMMENT_ID)
                .content("comment content")
                .user(user)
                .post(post)
                .build();

    }

    @Test
    void createComment_PostNotFound() {
        // given
        when(postRepository.findById(POST_ID)).thenReturn(Optional.empty());

        // when & then
        assertThatThrownBy(() -> service.createComt(comment, null, POST_ID))
                .isInstanceOf(PostNotFoundException.class);
    }


    @Test
    void createComment_WithoutParent() {
        //given
        when(postRepository.findById(POST_ID)).thenReturn(Optional.of(post));
        when(repository.save(any(Comment.class))).thenReturn(comment);

        //when
        Comment savedComment = service.createComt(comment, null, POST_ID);

        //then
        assertThat(savedComment).isEqualTo(comment);
        assertThat(comment.getParentComment()).isNull();
        verify(postRepository).findById(POST_ID);
        verify(repository).save(comment);
    }

    @Test
    void createComment_WithParent() {
        //given
        when(repository.findById(PARENT_ID)).thenReturn(Optional.of(parentComment));
        when(postRepository.findById(POST_ID)).thenReturn(Optional.of(post));
        when(repository.save(any(Comment.class))).thenReturn(comment);

        //when
        Comment savedComment = service.createComt(comment, PARENT_ID, POST_ID);

        //then
        assertThat(comment.getParentComment()).isEqualTo(parentComment);
        assertThat(savedComment).isEqualTo(comment);
        verify(postRepository).findById(POST_ID);
        verify(repository).findById(PARENT_ID);
    }

    @Test
    void createComment_WithParentWrongId() {
        //given
        when(repository.findById(PARENT_ID)).thenReturn(Optional.empty());
        when(postRepository.findById(POST_ID)).thenReturn(Optional.of(post));

        //when & then
        assertThatThrownBy(() -> service.createComt(comment, PARENT_ID, POST_ID))
                .isInstanceOf(CommentNotFoundException.class);
    }

    @Test
    void viewComments() {
        //given
        when(repository.findByPostId(POST_ID)).thenReturn(List.of(comment, parentComment));

        //when
        List<Comment> comments = service.viewComts(POST_ID);

        //then
        assertThat(comments.size()).isEqualTo(2);
        verify(repository).findByPostId(POST_ID);
    }

    @Test
    void viewComments_EmptyList() {
        // given
        when(repository.findByPostId(POST_ID)).thenReturn(Collections.emptyList());

        // when
        List<Comment> comments = service.viewComts(POST_ID);

        // then
        assertThat(comments).isEmpty();
        verify(repository).findByPostId(POST_ID);
    }
}

photo

이미지 파일을 담당할 PhotoService의 단위 테스트입니다. 프로젝트 내 테스트 파일 저장 경로를 지정하여 임시 저장, 업로드에 관한 부분이 정상 작동하는지 테스트하였습니다. 저장소에 파일 IO 처리가 발생하는 로직이다 보니 Assertions를 통한 검증과 @AfterEach 내용을 주석 처리한 뒤 생성 여부와 본문 내 파일 경로 변경을 직접 확인하였습니다. 

@Slf4j
@ExtendWith(MockitoExtension.class)
class PhotoServiceTest {

    @Mock
    PhotoRepository photoRepository;
    @Mock
    PostRepository postRepository;
    @Mock
    MultipartFile multipartFile;
    @Mock
    MultipartFile multipartFile2;

    @InjectMocks
    PhotoService service;

    static final String SESSION_ID = "user";
    static final String ORIGINAL_FILE_NAME = "test.jpg";
    static final String ORIGINAL_FILE_NAME2 = "test2.jpg";
    static final String TEST_UPLOAD_PATH = "src/test/resources/test-uploads/";

    @BeforeEach
    void setUp() {
        // 테스트용 업로드 디렉토리 생성
        new File(TEST_UPLOAD_PATH).mkdirs();
        // UPLOAD_PATH 값을 테스트용 경로로 변경
        ReflectionTestUtils.setField(service, "UPLOAD_PATH", TEST_UPLOAD_PATH);

    }

    @AfterEach
    void cleanup() {
        // 테스트 후 생성된 파일들 삭제
        FileSystemUtils.deleteRecursively(new File(TEST_UPLOAD_PATH));
    }

    @Test
    void tempUploadPhoto_Success() throws IOException {
        // given
        when(multipartFile.getOriginalFilename()).thenReturn(ORIGINAL_FILE_NAME);

        // transferTo 메서드 동작 정의 추가
        doAnswer(invocation -> {
            File file = invocation.getArgument(0);
            new FileOutputStream(file).close();
            return null;
        }).when(multipartFile).transferTo(any(File.class));

        // when
        String tempFilePath = service.tempUploadPhoto(multipartFile, SESSION_ID);

        // then
        assertThat(tempFilePath).startsWith("/images/temp_");
        assertThat(tempFilePath).endsWith(".jpg");

        // 임시 파일이 실제로 생성되었는지 확인
        File tempFile = new File(TEST_UPLOAD_PATH + tempFilePath.substring(8));
        assertThat(tempFile).exists();
    }

    @Test
    void tempUploadPhoto_InvalidFileName() {
        // given
        when(multipartFile.getOriginalFilename()).thenReturn(null);

        // when & then
        assertThatThrownBy(() -> service.tempUploadPhoto(multipartFile, SESSION_ID))
                .isInstanceOf(IllegalArgumentException.class);
    }

    @Test
    void tempUploadPhoto_IOExceptionCase() throws IOException {
        // given
        when(multipartFile.getOriginalFilename()).thenReturn("test.jpg");
        doThrow(new IOException()).when(multipartFile).transferTo(any(File.class));

        // when & then
        assertThatThrownBy(() -> service.tempUploadPhoto(multipartFile, SESSION_ID))
                .isInstanceOf(FileUploadException.class);
    }


    @Test
    void uploadPhoto_Success() throws IOException {
        // given
        when(multipartFile.getOriginalFilename()).thenReturn(ORIGINAL_FILE_NAME);
        when(multipartFile2.getOriginalFilename()).thenReturn(ORIGINAL_FILE_NAME2);

        doAnswer(invocation -> {
            File file = invocation.getArgument(0);
            new FileOutputStream(file).close();
            return null;
        }).when(multipartFile).transferTo(any(File.class));

        doAnswer(invocation -> {
            File file = invocation.getArgument(0);
            new FileOutputStream(file).close();
            return null;
        }).when(multipartFile2).transferTo(any(File.class));

        //임시 파일 생성
        String tempFilePath = service.tempUploadPhoto(multipartFile, SESSION_ID);
        File tempFile = new File(tempFilePath);

        String tempFilePath2 = service.tempUploadPhoto(multipartFile2, SESSION_ID);
        File tempFile2 = new File(tempFilePath2);

        //임시 파일 주소로 post 생성
        Long postId = 1L;
        String con = "게시글 본문 내용 중 이미지가 포함되는 형식<img src='"
                + tempFilePath + "'>이미지가 가운데 포함되어 있음"
                + "<img src='" + tempFilePath2 + "'>두 번째 이미지가 포함됨";

        Post post = Post.builder()
                .id(postId)
                .content(con)
                .build();


        when(postRepository.findById(postId)).thenReturn(Optional.of(post));
        when(photoRepository.save(any(Photo.class))).thenAnswer(i -> i.getArgument(0));

        // when
        List<Photo> photos = service.uploadPhoto(postId , SESSION_ID);

        // then
        // 1. 임시 파일이 정식 파일로 변환되었는지 확인
        assertThat(tempFile).doesNotExist();
        assertThat(tempFile2).doesNotExist();
        assertThat(photos).hasSize(2);

        // 2. 본문의 이미지 경로가 업데이트되었는지 확인
        assertThat(post.getContent()).doesNotContain(tempFilePath.substring(8));
        assertThat(post.getContent()).contains(photos.get(0).getSavedPhotoName());
        assertThat(post.getContent()).doesNotContain(tempFilePath2.substring(8));
        assertThat(post.getContent()).contains(photos.get(1).getSavedPhotoName());

        // 3. tempFileMap에서 해당 postId 정보가 삭제되었는지 확인
        Map<String, Set<String>> tempFileMap = (Map<String, Set<String>>) ReflectionTestUtils.getField(service, "tempFileMap");
        assertThat(tempFileMap).doesNotContainKey(SESSION_ID);

        // 4. 직접 확인
        log.info("content = {}", con);
        log.info("post.getContent = {}", post.getContent());
    }

    @Test
    void uploadPhoto_PostNotFound() {
        // given
        when(postRepository.findById(any())).thenReturn(Optional.empty());

        // when & then
        assertThatThrownBy(() ->
                service.uploadPhoto(1L, SESSION_ID))
                .isInstanceOf(PostNotFoundException.class);
    }

    @Test
    void uploadPhoto_NoTempFiles() {
        // given
        Post post = Post.builder().id(1L).content("content").build();
        when(postRepository.findById(1L)).thenReturn(Optional.of(post));

        // when
        List<Photo> photos = service.uploadPhoto(1L, SESSION_ID);

        // then
        assertThat(photos).isEmpty();
    }

    @Test
    void viewPhotos_Success() {
        // given
        Long postId = 1L;
        List<Photo> photoList = List.of(Photo.builder().build(), Photo.builder().build());
        when(photoRepository.findByPostId(postId)).thenReturn(photoList);

        // when
        List<Photo> result = service.viewPhotos(postId);

        // then
        assertThat(result).hasSize(2);
        verify(photoRepository).findByPostId(postId);
    }

    @Test
    void viewPhotos_EmptyList() {
        // given
        when(photoRepository.findByPostId(any())).thenReturn(Collections.emptyList());

        // when
        List<Photo> result = service.viewPhotos(1L);

        // then
        assertThat(result).isEmpty();
    }
}

 

728x90